first commit

This commit is contained in:
User A0264400
2026-04-01 23:20:16 +03:00
commit a766acdc90
23071 changed files with 4933189 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
<?php
namespace Automattic\WooCommerce\Blocks\AI;
/**
* Class Configuration
*
* @internal
* @deprecated This class can't be removed due https://github.com/woocommerce/woocommerce/issues/52311.
*/
class Configuration {}

View File

@@ -0,0 +1,11 @@
<?php
namespace Automattic\WooCommerce\Blocks\AI;
/**
* Class Connection
*
* @internal
* @deprecated This class can't be removed due https://github.com/woocommerce/woocommerce/issues/52311.
*/
class Connection {}

View File

@@ -0,0 +1,13 @@
<?php
namespace Automattic\WooCommerce\Blocks\AIContent;
/**
* ContentProcessor class.
*
* Process images for content
*
* @internal
* @deprecated This class can't be removed due https://github.com/woocommerce/woocommerce/issues/52311.
*/
class ContentProcessor {}

View File

@@ -0,0 +1,12 @@
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Blocks\AIContent;
/**
* Patterns Dictionary class.
*
* @internal
* @deprecated This class can't be removed due https://github.com/woocommerce/woocommerce/issues/52311.
*/
class PatternsDictionary {}

View File

@@ -0,0 +1,11 @@
<?php
namespace Automattic\WooCommerce\Blocks\AIContent;
/**
* Patterns Helper class.
*
* @internal
* @deprecated This class can't be removed due https://github.com/woocommerce/woocommerce/issues/52311.
*/
class PatternsHelper {}

View File

@@ -0,0 +1,12 @@
<?php
namespace Automattic\WooCommerce\Blocks\AIContent;
/**
* Pattern Images class.
*
* @internal
* @deprecated This class can't be removed due https://github.com/woocommerce/woocommerce/issues/52311.
*/
class UpdatePatterns {
}

View File

@@ -0,0 +1,240 @@
<?php
namespace Automattic\WooCommerce\Blocks\AIContent;
use WP_Error;
/**
* This class is used to create dummy products for the Customize Your Store flow.
* Even if it is in the AI Content namespace, it is not used for AI content generation.
*
* @internal
*/
class UpdateProducts {
/**
* The dummy products.
*/
const DUMMY_PRODUCTS = [
[
'title' => 'Vintage Typewriter',
'image' => 'assets/images/pattern-placeholders/writing-typing-keyboard-technology-white-vintage.jpg',
'description' => 'A hit spy novel or a love letter? Anything you type using this vintage typewriter from the 20s is bound to make a mark.',
'price' => 90,
],
[
'title' => 'Leather-Clad Leisure Chair',
'image' => 'assets/images/pattern-placeholders/table-wood-house-chair-floor-window.jpg',
'description' => 'Sit back and relax in this comfy designer chair. High-grain leather and steel frame add luxury to your your leisure.',
'price' => 249,
],
[
'title' => 'Black and White',
'image' => 'assets/images/pattern-placeholders/white-black-black-and-white-photograph-monochrome-photography.jpg',
'description' => 'This 24" x 30" high-quality print just exudes summer. Hang it on the wall and forget about the world outside.',
'price' => 115,
],
[
'title' => '3-Speed Bike',
'image' => 'assets/images/pattern-placeholders/road-sport-vintage-wheel-retro-old.jpg',
'description' => 'Zoom through the streets on this premium 3-speed bike. Manufactured and assembled in Germany in the 80s.',
'price' => 115,
],
[
'title' => 'Hi-Fi Headphones',
'image' => 'assets/images/pattern-placeholders/man-person-music-black-and-white-white-photography.jpg',
'description' => 'Experience your favorite songs in a new way with these premium hi-fi headphones.',
'price' => 125,
],
[
'title' => 'Retro Glass Jug (330 ml)',
'image' => 'assets/images/pattern-placeholders/drinkware-liquid-tableware-dishware-bottle-fluid.jpg',
'description' => 'Thick glass and a classic silhouette make this jug a must-have for any retro-inspired kitchen.',
'price' => 115,
],
];
/**
* Return all dummy products that were not modified by the store owner.
*
* @return array|WP_Error An array with the dummy products that need to have their content updated by AI.
*/
public function fetch_dummy_products_to_update() {
$real_products = $this->fetch_product_ids();
$real_products_count = count( $real_products );
if ( is_array( $real_products ) && $real_products_count > 6 ) {
return array(
'product_content' => array(),
);
}
$dummy_products = $this->fetch_product_ids( 'dummy' );
$dummy_products_count = count( $dummy_products );
$products_to_create = max( 0, 6 - $real_products_count - $dummy_products_count );
while ( $products_to_create > 0 ) {
$this->create_new_product( self::DUMMY_PRODUCTS[ $products_to_create - 1 ] );
--$products_to_create;
}
// Identify dummy products that need to have their content updated.
$dummy_products_ids = $this->fetch_product_ids( 'dummy' );
if ( ! is_array( $dummy_products_ids ) ) {
return new \WP_Error( 'failed_to_fetch_dummy_products', __( 'Failed to fetch dummy products.', 'woocommerce' ) );
}
$dummy_products = array_map(
function ( $product ) {
return wc_get_product( $product->ID );
},
$dummy_products_ids
);
$dummy_products_to_update = [];
foreach ( $dummy_products as $dummy_product ) {
if ( ! $dummy_product instanceof \WC_Product ) {
continue;
}
$should_update_dummy_product = $this->should_update_dummy_product( $dummy_product );
if ( $should_update_dummy_product ) {
$dummy_products_to_update[] = $dummy_product;
}
}
return $dummy_products_to_update;
}
/**
* Verify if the dummy product should have its content generated and managed by AI.
*
* @param \WC_Product $dummy_product The dummy product.
*
* @return bool
*/
public function should_update_dummy_product( $dummy_product ): bool {
$date_created = $dummy_product->get_date_created();
$date_modified = $dummy_product->get_date_modified();
if ( ! $date_created instanceof \WC_DateTime || ! $date_modified instanceof \WC_DateTime ) {
return false;
}
$formatted_date_created = $dummy_product->get_date_created()->date( 'Y-m-d H:i:s' );
$formatted_date_modified = $dummy_product->get_date_modified()->date( 'Y-m-d H:i:s' );
$timestamp_created = strtotime( $formatted_date_created );
$timestamp_modified = strtotime( $formatted_date_modified );
$timestamp_current = time();
$dummy_product_recently_modified = abs( $timestamp_current - $timestamp_modified ) < 10;
$dummy_product_not_modified = abs( $timestamp_modified - $timestamp_created ) < 60;
if ( $dummy_product_not_modified || $dummy_product_recently_modified ) {
return true;
}
return false;
}
/**
* Creates a new product and assigns the _headstart_post meta to it.
*
* @param array $product_data The product data.
*
* @return bool|int|\WP_Error
*/
public function create_new_product( $product_data ) {
$product = new \WC_Product();
$image_src = plugins_url( $product_data['image'], dirname( __DIR__, 2 ) );
$image_alt = $product_data['title'];
$product_image_id = $this->product_image_upload( $product->get_id(), $image_src, $image_alt );
$saved_product = $this->product_update( $product, $product_image_id, $product_data['title'], $product_data['description'], $product_data['price'] );
if ( is_wp_error( $saved_product ) ) {
return $saved_product;
}
return update_post_meta( $saved_product, '_headstart_post', true );
}
/**
* Return all existing products that have the _headstart_post meta assigned to them.
*
* @param string $type The type of products to fetch.
*
* @return array|null
*/
public function fetch_product_ids( string $type = 'user_created' ) {
global $wpdb;
if ( 'user_created' === $type ) {
return $wpdb->get_results( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE ID NOT IN ( SELECT p.ID FROM {$wpdb->posts} p JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id WHERE pm.meta_key = %s AND p.post_type = 'product' AND p.post_status = 'publish' ) AND post_type = 'product' AND post_status = 'publish' LIMIT 6", '_headstart_post' ) );
}
return $wpdb->get_results( $wpdb->prepare( "SELECT p.ID FROM {$wpdb->posts} p JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id WHERE pm.meta_key = %s AND p.post_type = 'product' AND p.post_status = 'publish'", '_headstart_post' ) );
}
/**
* Upload the image for the product.
*
* @param int $product_id The product ID.
* @param string $image_src The image source.
* @param string $image_alt The image alt.
*
* @return int|string|WP_Error
*/
private function product_image_upload( $product_id, $image_src, $image_alt ) {
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/image.php';
// Since the media_sideload_image function is expensive and can take longer to complete
// the process of downloading the external image and uploading it to the media library,
// here we are increasing the time limit to avoid any issues.
set_time_limit( 150 );
wp_raise_memory_limit( 'image' );
return media_sideload_image( $image_src, $product_id, $image_alt, 'id' );
}
/**
* Update the product with the new content.
*
* @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
*/
private function product_update( $product, $product_image_id, $product_title, $product_description, $product_price ) {
if ( ! $product instanceof \WC_Product ) {
return new WP_Error( 'invalid_product', __( 'Invalid product.', 'woocommerce' ) );
}
if ( ! is_wp_error( $product_image_id ) ) {
$product->set_image_id( $product_image_id );
} else {
wc_get_logger()->warning(
sprintf(
// translators: %s is a generated error message.
__( 'The image upload failed: "%s", creating the product without image', 'woocommerce' ),
$product_image_id->get_error_message()
),
);
}
$product->set_name( $product_title );
$product->set_description( $product_description );
$product->set_price( $product_price );
$product->set_regular_price( $product_price );
$product->set_slug( sanitize_title( $product_title ) );
$product->save();
return $product->get_id();
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace Automattic\WooCommerce\Blocks;
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\Blocks\Assets\Api as AssetApi;
/**
* Assets class.
*
* @deprecated 5.0.0 This class will be removed in a future release. This has been replaced by AssetsController.
* @internal
*/
class Assets {
/**
* Initialize class features on init.
*
* @since 2.5.0
* @deprecated 5.0.0
*/
public static function init() {
_deprecated_function( 'Assets::init', '5.0.0' );
}
/**
* Register block scripts & styles.
*
* @since 2.5.0
* @deprecated 5.0.0
*/
public static function register_assets() {
_deprecated_function( 'Assets::register_assets', '5.0.0' );
}
/**
* Register the vendors style file. We need to do it after the other files
* because we need to check if `wp-edit-post` has been enqueued.
*
* @deprecated 5.0.0
*/
public static function enqueue_scripts() {
_deprecated_function( 'Assets::enqueue_scripts', '5.0.0' );
}
/**
* Add body classes.
*
* @deprecated 5.0.0
* @param array $classes Array of CSS classnames.
* @return array Modified array of CSS classnames.
*/
public static function add_theme_body_class( $classes = [] ) {
_deprecated_function( 'Assets::add_theme_body_class', '5.0.0' );
return $classes;
}
/**
* Add theme class to admin body.
*
* @deprecated 5.0.0
* @param array $classes String with the CSS classnames.
* @return array Modified string of CSS classnames.
*/
public static function add_theme_admin_body_class( $classes = '' ) {
_deprecated_function( 'Assets::add_theme_admin_body_class', '5.0.0' );
return $classes;
}
/**
* Adds a redirect field to the login form so blocks can redirect users after login.
*
* @deprecated 5.0.0
*/
public static function redirect_to_field() {
_deprecated_function( 'Assets::redirect_to_field', '5.0.0' );
}
/**
* Queues a block script in the frontend.
*
* @since 2.3.0
* @since 2.6.0 Changed $name to $script_name and added $handle argument.
* @since 2.9.0 Made it so scripts are not loaded in admin pages.
* @deprecated 4.5.0 Block types register the scripts themselves.
*
* @param string $script_name Name of the script used to identify the file inside build folder.
* @param string $handle Optional. Provided if the handle should be different than the script name. `wc-` prefix automatically added.
* @param array $dependencies Optional. An array of registered script handles this script depends on. Default empty array.
*/
public static function register_block_script( $script_name, $handle = '', $dependencies = [] ) {
_deprecated_function( 'register_block_script', '4.5.0' );
$asset_api = Package::container()->get( AssetApi::class );
$asset_api->register_block_script( $script_name, $handle, $dependencies );
}
}

View File

@@ -0,0 +1,362 @@
<?php
namespace Automattic\WooCommerce\Blocks\Assets;
use Automattic\WooCommerce\Blocks\Domain\Package;
use Exception;
use Automattic\Jetpack\Constants;
/**
* The Api class provides an interface to various asset registration helpers.
*
* Contains asset api methods
*
* @since 2.5.0
*/
class Api {
/**
* Stores the prefixed WC version. Used because the WC Blocks version has not been updated since the monorepo merge.
*
* @var string
*/
public $wc_version;
/**
* Stores inline scripts already enqueued.
*
* @var array
*/
private $inline_scripts = [];
/**
* Determines if caching is enabled for script data.
*
* @var boolean
*/
private $disable_cache = false;
/**
* Stores loaded script data for the current request
*
* @var array|null
*/
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.
*
* @var string
*/
private $script_data_hash;
/**
* Stores the transient key used to cache the script data. This will change if the site is accessed via HTTPS or HTTP.
*
* @var string
*/
private $script_data_transient_key = 'woocommerce_blocks_asset_api_script_data';
/**
* Reference to the Package instance
*
* @var Package
*/
private $package;
/**
* Constructor for class
*
* @param Package $package An instance of Package.
*/
public function __construct( Package $package ) {
// Use wc- prefix here to prevent collisions when WC Core version catches up to a version previously used by the WC Blocks feature plugin.
$this->wc_version = 'wc-' . Constants::get_constant( 'WC_VERSION' );
$this->package = $package;
$this->disable_cache = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) || wp_get_environment_type() !== 'production';
// If the site is accessed via HTTPS, change the transient key. This is to prevent the script URLs being cached
// with the first scheme they are accessed on after cache expiry.
if ( is_ssl() ) {
$this->script_data_transient_key .= '_ssl';
}
if ( ! $this->disable_cache ) {
$this->script_data_hash = $this->get_script_data_hash();
}
add_action( 'shutdown', array( $this, 'update_script_data_cache' ), 20 );
}
/**
* Get the file modified time as a cache buster if we're in dev mode.
*
* @param string $file Local path to the file (relative to the plugin
* directory).
* @return string The cache buster value to use for the given file.
*/
protected function get_file_version( $file ) {
if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG && file_exists( $this->package->get_path() . $file ) ) {
return filemtime( $this->package->get_path( trim( $file, '/' ) ) );
}
return $this->wc_version;
}
/**
* Retrieve the url to an asset for this plugin.
*
* @param string $relative_path An optional relative path appended to the
* returned url.
*
* @return string
*/
protected function get_asset_url( $relative_path = '' ) {
return $this->package->get_url( $relative_path );
}
/**
* Get the path to a block's metadata
*
* @param string $block_name The block to get metadata for.
* @param string $path Optional. The path to the metadata file inside the 'assets/client/blocks' folder.
*
* @return string|boolean False if metadata file is not found for the block.
*/
public function get_block_metadata_path( $block_name, $path = '' ) {
$path_to_metadata_from_plugin_root = $this->package->get_path( 'assets/client/blocks/' . $path . $block_name . '/block.json' );
if ( ! file_exists( $path_to_metadata_from_plugin_root ) ) {
return false;
}
return $path_to_metadata_from_plugin_root;
}
/**
* Generates a hash containing the site url, plugin version and package path.
*
* Moving the plugin, changing the version, or changing the site url will result in a new hash and the cache will be invalidated.
*
* @return string The generated hash.
*/
private function get_script_data_hash() {
return md5( get_option( 'siteurl', '' ) . $this->wc_version . $this->package->get_path() );
}
/**
* Initialize and load cached script data from the transient cache.
*
* @return array
*/
private function get_cached_script_data() {
if ( $this->disable_cache ) {
return [];
}
$transient_value = json_decode( (string) get_transient( $this->script_data_transient_key ), true );
if (
json_last_error() !== JSON_ERROR_NONE ||
empty( $transient_value ) ||
empty( $transient_value['script_data'] ) ||
empty( $transient_value['version'] ) ||
$transient_value['version'] !== $this->wc_version ||
empty( $transient_value['hash'] ) ||
$transient_value['hash'] !== $this->script_data_hash
) {
return [];
}
return (array) ( $transient_value['script_data'] ?? [] );
}
/**
* Store all cached script data in the transient cache.
*/
public function update_script_data_cache() {
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(
array(
'script_data' => $this->script_data,
'version' => $this->wc_version,
'hash' => $this->script_data_hash,
)
),
DAY_IN_SECONDS * 30
);
}
/**
* Use package path to find an asset data file and return the data.
*
* @param string $filename The filename of the asset.
* @return array The asset data.
*/
public function get_asset_data( $filename ) {
$asset_path = $this->package->get_path( $filename );
$asset = file_exists( $asset_path ) ? require $asset_path : [];
return $asset;
}
/**
* Get src, version and dependencies given a script relative src.
*
* @param string $relative_src Relative src to the script.
* @param array $dependencies Optional. An array of registered script handles this script depends on. Default empty array.
*
* @return array src, version and dependencies of the script.
*/
public function get_script_data( $relative_src, $dependencies = [] ) {
if ( ! $relative_src ) {
return array(
'src' => '',
'version' => '1',
'dependencies' => $dependencies,
);
}
if ( is_null( $this->script_data ) ) {
$this->script_data = $this->get_cached_script_data();
}
if ( empty( $this->script_data[ $relative_src ] ) ) {
$asset_path = $this->package->get_path( str_replace( '.js', '.asset.php', $relative_src ) );
// The following require is safe because we are checking if the file exists and it is not a user input.
// nosemgrep audit.php.lang.security.file.inclusion-arg.
$asset = file_exists( $asset_path ) ? require $asset_path : [];
$this->script_data[ $relative_src ] = array(
'src' => $this->get_asset_url( $relative_src ),
'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.
return [
'src' => $this->script_data[ $relative_src ]['src'],
'version' => $this->script_data[ $relative_src ]['version'],
'dependencies' => array_merge( $this->script_data[ $relative_src ]['dependencies'], $dependencies ),
];
}
/**
* Registers a script according to `wp_register_script`, adding the correct prefix, and additionally loading translations.
*
* When creating script assets, the following rules should be followed:
* 1. All asset handles should have a `wc-` prefix.
* 2. If the asset handle is for a Block (in editor context) use the `-block` suffix.
* 3. If the asset handle is for a Block (in frontend context) use the `-block-frontend` suffix.
* 4. If the asset is for any other script being consumed or enqueued by the blocks plugin, use the `wc-blocks-` prefix.
*
* @since 2.5.0
* @throws Exception If the registered script has a dependency on itself.
*
* @param string $handle Unique name of the script.
* @param string $relative_src Relative url for the script to the path from plugin root.
* @param array $dependencies Optional. An array of registered script handles this script depends on. Default empty array.
* @param bool $has_i18n Optional. Whether to add a script translation call to this file. Default: true.
*/
public function register_script( $handle, $relative_src, $dependencies = [], $has_i18n = true ) {
$script_data = $this->get_script_data( $relative_src, $dependencies );
if ( in_array( $handle, $script_data['dependencies'], true ) ) {
if ( wp_get_environment_type() === 'development' ) {
$dependencies = array_diff( $script_data['dependencies'], [ $handle ] );
add_action(
'admin_notices',
function () use ( $handle ) {
echo '<div class="error"><p>';
/* translators: %s file handle name. */
printf( esc_html__( 'Script with handle %s had a dependency on itself which has been removed. This is an indicator that your JS code has a circular dependency that can cause bugs.', 'woocommerce' ), esc_html( $handle ) );
echo '</p></div>';
}
);
} else {
throw new Exception( sprintf( 'Script with handle %s had a dependency on itself. This is an indicator that your JS code has a circular dependency that can cause bugs.', $handle ) );
}
}
/**
* Filters the list of script dependencies.
*
* @since 3.0.0
*
* @param array $dependencies The list of script dependencies.
* @param string $handle The script's handle.
* @return array
*/
$script_dependencies = apply_filters( 'woocommerce_blocks_register_script_dependencies', $script_data['dependencies'], $handle );
wp_register_script( $handle, $script_data['src'], $script_dependencies, $script_data['version'], true );
if ( $has_i18n && function_exists( 'wp_set_script_translations' ) ) {
wp_set_script_translations( $handle, 'woocommerce', $this->package->get_path( 'languages' ) );
wp_set_script_translations( $handle, 'woocommerce', $this->package->get_path( 'i18n/languages' ) );
}
}
/**
* Registers a style according to `wp_register_style`.
*
* @since 2.5.0
* @since 2.6.0 Change src to be relative source.
*
* @param string $handle Name of the stylesheet. Should be unique.
* @param string $relative_src Relative source of the stylesheet to the plugin path.
* @param array $deps Optional. An array of registered stylesheet handles this stylesheet depends on. Default empty array.
* @param string $media Optional. The media for which this stylesheet has been defined. Default 'all'. Accepts media types like
* 'all', 'print' and 'screen', or media queries like '(orientation: portrait)' and '(max-width: 640px)'.
* @param boolean $rtl Optional. Whether or not to register RTL styles.
*/
public function register_style( $handle, $relative_src, $deps = [], $media = 'all', $rtl = false ) {
$filename = str_replace( plugins_url( '/', dirname( __DIR__ ) ), '', $relative_src );
$src = $this->get_asset_url( $relative_src );
$ver = $this->get_file_version( $filename );
wp_register_style( $handle, $src, $deps, $ver, $media );
if ( $rtl ) {
wp_style_add_data( $handle, 'rtl', 'replace' );
}
}
/**
* Returns the appropriate asset path for current builds.
*
* @param string $filename Filename for asset path (without extension).
* @param string $type File type (.css or .js).
* @return string The generated path.
*/
public function get_block_asset_build_path( $filename, $type = 'js' ) {
return "assets/client/blocks/$filename.$type";
}
/**
* Adds an inline script, once.
*
* @param string $handle Script handle.
* @param string $script Script contents.
*/
public function add_inline_script( $handle, $script ) {
if ( ! empty( $this->inline_scripts[ $handle ] ) && in_array( $script, $this->inline_scripts[ $handle ], true ) ) {
return;
}
wp_add_inline_script( $handle, $script );
if ( isset( $this->inline_scripts[ $handle ] ) ) {
$this->inline_scripts[ $handle ][] = $script;
} else {
$this->inline_scripts[ $handle ] = array( $script );
}
}
}

View File

@@ -0,0 +1,444 @@
<?php
namespace Automattic\WooCommerce\Blocks\Assets;
use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\Blocks\Domain\Services\Hydration;
use Automattic\WooCommerce\Internal\Logging\RemoteLogger;
use Exception;
use InvalidArgumentException;
/**
* Class instance for registering data used on the current view session by
* assets.
*
* Holds data registered for output on the current view session when
* `wc-settings` is enqueued( directly or via dependency )
*
* @since 2.5.0
*/
class AssetDataRegistry {
/**
* Contains registered data.
*
* @var array
*/
private $data = [];
/**
* Contains preloaded API data.
*
* @var array
*/
private $preloaded_api_requests = [];
/**
* Lazy data is an array of closures that will be invoked just before
* asset data is generated for the enqueued script.
*
* @var array
*/
private $lazy_data = [];
/**
* Asset handle for registered data.
*
* @var string
*/
private $handle = 'wc-settings';
/**
* Asset API interface for various asset registration.
*
* @var API
*/
private $api;
/**
* Constructor
*
* @param Api $asset_api Asset API interface for various asset registration.
*/
public function __construct( Api $asset_api ) {
$this->api = $asset_api;
$this->init();
}
/**
* Hook into WP asset registration for enqueueing asset data.
*/
protected function init() {
add_action( 'init', array( $this, 'register_data_script' ) );
add_action( is_admin() ? 'admin_print_footer_scripts' : 'wp_print_footer_scripts', array( $this, 'enqueue_asset_data' ), 1 );
}
/**
* Exposes core data via the wcSettings global. This data is shared throughout the client.
*
* Settings that are used by various components or multiple blocks should be added here. Note, that settings here are
* global so be sure not to add anything heavy if possible.
*
* @return array An array containing core data.
*/
protected function get_core_data() {
return [
'adminUrl' => admin_url(),
'countries' => WC()->countries->get_countries(),
'currency' => $this->get_currency_data(),
'currentUserId' => get_current_user_id(),
'currentUserIsAdmin' => current_user_can( 'manage_woocommerce' ),
'currentThemeIsFSETheme' => wp_is_block_theme(),
'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(),
'productsSettings' => $this->get_products_settings(),
'siteTitle' => wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ),
'storePages' => $this->get_store_pages(),
'wcAssetUrl' => plugins_url( 'assets/', WC_PLUGIN_FILE ),
'wcVersion' => defined( 'WC_VERSION' ) ? WC_VERSION : '',
'wpLoginUrl' => wp_login_url(),
'wpVersion' => get_bloginfo( 'version' ),
];
}
/**
* Get currency data to include in settings.
*
* @return array
*/
protected function get_currency_data() {
$currency = get_woocommerce_currency();
return [
'code' => $currency,
'precision' => wc_get_price_decimals(),
'symbol' => html_entity_decode( get_woocommerce_currency_symbol( $currency ) ),
'symbolPosition' => get_option( 'woocommerce_currency_pos' ),
'decimalSeparator' => wc_get_price_decimal_separator(),
'thousandSeparator' => wc_get_price_thousand_separator(),
'priceFormat' => html_entity_decode( get_woocommerce_price_format() ),
];
}
/**
* Get locale data to include in settings.
*
* @return array
*/
protected function get_locale_data() {
global $wp_locale;
return [
'siteLocale' => get_locale(),
'userLocale' => get_user_locale(),
'weekdaysShort' => array_values( $wp_locale->weekday_abbrev ),
];
}
/**
* Get store pages to include in settings.
*
* @return array
*/
protected function get_store_pages() {
$store_pages = [
'myaccount' => wc_get_page_id( 'myaccount' ),
'shop' => wc_get_page_id( 'shop' ),
'cart' => wc_get_page_id( 'cart' ),
'checkout' => wc_get_page_id( 'checkout' ),
'privacy' => wc_privacy_policy_page_id(),
'terms' => wc_terms_and_conditions_page_id(),
];
if ( is_callable( '_prime_post_caches' ) ) {
_prime_post_caches( array_values( $store_pages ), false, false );
}
return array_map(
[ $this, 'format_page_resource' ],
$store_pages
);
}
/**
* Get product related settings.
*
* Note: For the time being we are exposing only the settings that are used by blocks.
*
* @return array
*/
protected function get_products_settings() {
return [
'cartRedirectAfterAdd' => get_option( 'woocommerce_cart_redirect_after_add' ) === 'yes',
];
}
/**
* Format a page object into a standard array of data.
*
* @param WP_Post|int $page Page object or ID.
* @return array
*/
protected function format_page_resource( $page ) {
if ( is_numeric( $page ) && $page > 0 ) {
$page = get_post( $page );
}
if ( ! is_a( $page, '\WP_Post' ) || 'publish' !== $page->post_status ) {
return [
'id' => 0,
'title' => '',
'permalink' => false,
];
}
return [
'id' => $page->ID,
'title' => $page->post_title,
'permalink' => get_permalink( $page->ID ),
];
}
/**
* Returns block-related data for enqueued wc-settings script.
* Format order statuses by removing a leading 'wc-' if present.
*
* @return array formatted statuses.
*/
protected function get_order_statuses() {
$formatted_statuses = array();
foreach ( wc_get_order_statuses() as $key => $value ) {
$formatted_key = preg_replace( '/^wc-/', '', $key );
$formatted_statuses[ $formatted_key ] = $value;
}
return $formatted_statuses;
}
/**
* Used for on demand initialization of asset data and registering it with
* the internal data registry.
*
* Note: core data will overwrite any externally registered data via the api.
*/
protected function initialize_core_data() {
/**
* Filters the array of shared settings.
*
* Low level hook for registration of new data late in the cycle. This is deprecated.
* Instead, use the data api:
*
* ```php
* Automattic\WooCommerce\Blocks\Package::container()->get( Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry::class )->add( $key, $value )
* ```
*
* @since 5.0.0
*
* @deprecated
* @param array $data Settings data.
* @return array
*/
$settings = apply_filters( 'woocommerce_shared_settings', $this->data );
// Surface a deprecation warning in the error console.
if ( has_filter( 'woocommerce_shared_settings' ) ) {
$error_handle = 'deprecated-shared-settings-error';
$error_message = '`woocommerce_shared_settings` filter in Blocks is deprecated. See https://github.com/woocommerce/woocommerce-gutenberg-products-block/blob/trunk/docs/contributors/block-assets.md';
// phpcs:ignore WordPress.WP.EnqueuedResourceParameters.NotInFooter,WordPress.WP.EnqueuedResourceParameters.MissingVersion
wp_register_script( $error_handle, '' );
wp_enqueue_script( $error_handle );
wp_add_inline_script(
$error_handle,
sprintf( 'console.warn( "%s" );', $error_message )
);
}
$core_data = $this->get_core_data();
$core_data['experimentalWcRestApiV4'] = Features::is_enabled( 'rest-api-v4' );
// note this WILL wipe any data already registered to these keys because they are protected.
$this->data = array_replace_recursive( $settings, $core_data );
}
/**
* Loops through each registered lazy data callback and adds the returned
* value to the data array.
*
* This method is executed right before preparing the data for printing to
* the rendered screen.
*
* @return void
*/
protected function execute_lazy_data() {
foreach ( $this->lazy_data as $key => $callback ) {
$this->data[ $key ] = $callback();
}
}
/**
* Exposes private registered data to child classes.
*
* @return array The registered data on the private data property
*/
protected function get() {
return $this->data;
}
/**
* Allows checking whether a key exists.
*
* @param string $key The key to check if exists.
* @return bool Whether the key exists in the current data registry.
*/
public function exists( $key ) {
return array_key_exists( $key, $this->data );
}
/**
* Interface for adding data to the registry.
*
* You can only register data that is not already in the registry identified by the given key. If there is a
* duplicate found, unless $ignore_duplicates is true, an exception will be thrown.
*
* @param string $key The key used to reference the data being registered. This should use camelCase.
* @param mixed $data If not a function, registered to the registry as is. If a function, then the
* callback is invoked right before output to the screen.
* @param boolean $check_key_exists Deprecated. If set to true, duplicate data will be ignored if the key exists.
* If false, duplicate data will cause an exception.
*/
public function add( $key, $data, $check_key_exists = false ) {
if ( $check_key_exists ) {
wc_deprecated_argument( 'Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry::add()', '8.9', 'The $check_key_exists parameter is no longer used: all duplicate data will be ignored if the key exists by default' );
}
$this->add_data( $key, $data );
}
/**
* Hydrate from the API.
*
* @param string $path REST API path to preload.
*/
public function hydrate_api_request( $path ) {
if ( ! isset( $this->preloaded_api_requests[ $path ] ) ) {
$this->preloaded_api_requests[ $path ] = Package::container()->get( Hydration::class )->get_rest_api_response_data( $path );
}
}
/**
* Hydrate some data from the API.
*
* @param string $key The key used to reference the data being registered.
* @param string $path REST API path to preload.
* @param boolean $check_key_exists If set to true, duplicate data will be ignored if the key exists.
* If false, duplicate data will cause an exception.
*
* @throws InvalidArgumentException Only throws when site is in debug mode. Always logs the error.
*/
public function hydrate_data_from_api_request( $key, $path, $check_key_exists = false ) {
$this->add(
$key,
function () use ( $path ) {
if ( isset( $this->preloaded_api_requests[ $path ], $this->preloaded_api_requests[ $path ]['body'] ) ) {
return $this->preloaded_api_requests[ $path ]['body'];
}
$response = Package::container()->get( Hydration::class )->get_rest_api_response_data( $path );
return $response['body'] ?? '';
},
$check_key_exists
);
}
/**
* Adds a page permalink to the data registry.
*
* @param integer $page_id Page ID to add to the registry.
*/
public function register_page_id( $page_id ) {
$permalink = $page_id ? get_permalink( $page_id ) : false;
if ( $permalink ) {
$this->data[ 'page-' . $page_id ] = $permalink;
}
}
/**
* Callback for registering the data script via WordPress API.
*
* @return void
*/
public function register_data_script() {
$this->api->register_script(
$this->handle,
'assets/client/blocks/wc-settings.js',
[ 'wp-api-fetch' ],
true
);
}
/**
* Callback for enqueuing asset data via the WP api.
*
* Note: while this is hooked into print/admin_print_scripts, it still only
* happens if the script attached to `wc-settings` handle is enqueued. This
* is done to allow for any potentially expensive data generation to only
* happen for routes that need it.
*/
public function enqueue_asset_data() {
if ( wp_script_is( $this->handle, 'enqueued' ) ) {
$this->initialize_core_data();
$this->execute_lazy_data();
$data = rawurlencode( wp_json_encode( $this->data ) );
$wc_settings_script = "var wcSettings = JSON.parse( decodeURIComponent( '" . esc_js( $data ) . "' ) );";
$preloaded_api_requests_script = '';
if ( count( $this->preloaded_api_requests ) > 0 ) {
$preloaded_api_requests = rawurlencode( wp_json_encode( $this->preloaded_api_requests ) );
$preloaded_api_requests_script = "wp.apiFetch.use( wp.apiFetch.createPreloadingMiddleware( JSON.parse( decodeURIComponent( '" . esc_js( $preloaded_api_requests ) . "' ) ) ) );";
}
wp_add_inline_script(
$this->handle,
$wc_settings_script . $preloaded_api_requests_script,
'before'
);
}
}
/**
* See self::add() for docs.
*
* @param string $key Key for the data.
* @param mixed $data Value for the data.
*/
protected function add_data( $key, $data ) {
if ( ! is_string( $key ) ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
trigger_error( esc_html__( 'Key for the data being registered must be a string', 'woocommerce' ), E_USER_WARNING );
return;
}
if ( $this->exists( $key ) ) {
return;
}
if ( isset( $this->data[ $key ] ) ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
trigger_error( esc_html__( 'Overriding existing data with an already registered key is not allowed', 'woocommerce' ), E_USER_WARNING );
return;
}
if ( \is_callable( $data ) ) {
$this->lazy_data[ $key ] = $data;
return;
}
$this->data[ $key ] = $data;
}
/**
* Exposes whether the current site is in debug mode or not.
*
* @return boolean True means the site is in debug mode.
*/
protected function debug() {
return defined( 'WP_DEBUG' ) && WP_DEBUG;
}
}

View File

@@ -0,0 +1,534 @@
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Blocks;
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Blocks\Assets\Api as AssetApi;
use Automattic\WooCommerce\Admin\Features\Features;
/**
* AssetsController class.
*
* @since 5.0.0
* @internal
*/
final class AssetsController {
/**
* Asset API interface for various asset registration.
*
* @var AssetApi
*/
private $api;
/**
* Constructor.
*
* @param AssetApi $asset_api Asset API interface for various asset registration.
*/
public function __construct( AssetApi $asset_api ) {
$this->api = $asset_api;
$this->init();
}
/**
* Initialize class features.
*/
protected function init() { // phpcs:ignore WooCommerce.Functions.InternalInjectionMethod.MissingPublic
add_action( 'init', array( $this, 'register_assets' ) );
add_action( 'init', array( $this, 'register_script_modules' ) );
add_action( 'enqueue_block_editor_assets', array( $this, 'register_and_enqueue_site_editor_assets' ) );
add_filter( 'wp_resource_hints', array( $this, 'add_resource_hints' ), 10, 2 );
add_action( 'body_class', array( $this, 'add_theme_body_class' ), 1 );
add_action( 'admin_body_class', array( $this, 'add_theme_body_class' ), 1 );
add_action( 'admin_enqueue_scripts', array( $this, 'update_block_style_dependencies' ), 20 );
add_action( 'wp_enqueue_scripts', array( $this, 'update_block_settings_dependencies' ), 100 );
add_action( 'admin_enqueue_scripts', array( $this, 'update_block_settings_dependencies' ), 100 );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_wc_entities' ), 100 );
add_filter( 'js_do_concat', array( $this, 'skip_boost_minification_for_cart_checkout' ), 10, 2 );
if ( Features::is_enabled( 'experimental-iapi-runtime' ) ) {
// Run after the WordPress iAPI runtime has been registered by setting a lower priority.
add_filter( 'wp_default_scripts', array( $this, 'reregister_core_iapi_runtime' ), 20 );
}
}
/**
* Re-registers the iAPI runtime registered by WordPress Core/Gutenberg, allowing WooCommerce to register its own version of the iAPI runtime.
*/
public function reregister_core_iapi_runtime() {
$interactivity_api_asset_data = $this->api->get_asset_data(
$this->api->get_block_asset_build_path( 'interactivity-api-assets', 'php' )
);
foreach ( $interactivity_api_asset_data as $handle => $data ) {
$handle_without_js = str_replace( '.js', '', $handle );
if ( '@wordpress/interactivity' === $handle_without_js || '@wordpress/interactivity-router' === $handle_without_js ) {
wp_deregister_script_module( $handle_without_js );
}
wp_register_script_module( $handle_without_js, plugins_url( $this->api->get_block_asset_build_path( $handle_without_js ), dirname( __DIR__ ) ), $data['dependencies'], $data['version'] );
}
}
/**
* Register script modules.
*/
public function register_script_modules() {
// Right now we only have one script modules build for supported interactivity API powered block front-ends.
// We generate a combined asset file for that via DependencyExtractionWebpackPlugin to make registration more
// efficient.
$asset_data = $this->api->get_asset_data(
$this->api->get_block_asset_build_path( 'interactivity-blocks-frontend-assets', 'php' )
);
foreach ( $asset_data as $handle => $data ) {
$handle_without_js = str_replace( '.js', '', $handle );
wp_register_script_module( $handle_without_js, plugins_url( $this->api->get_block_asset_build_path( $handle_without_js ), dirname( __DIR__ ) ), $data['dependencies'], $data['version'] );
}
}
/**
* Register block scripts & styles.
*/
public function register_assets() {
$this->register_style( 'wc-blocks-packages-style', plugins_url( $this->api->get_block_asset_build_path( 'packages-style', 'css' ), dirname( __DIR__ ) ), array(), 'all', true );
$this->register_style( 'wc-blocks-style', plugins_url( $this->api->get_block_asset_build_path( 'wc-blocks', 'css' ), dirname( __DIR__ ) ), array(), 'all', true );
$this->register_style( 'wc-blocks-editor-style', plugins_url( $this->api->get_block_asset_build_path( 'wc-blocks-editor-style', 'css' ), dirname( __DIR__ ) ), array( 'wp-edit-blocks' ), 'all', true );
$this->api->register_script( 'wc-types', $this->api->get_block_asset_build_path( 'wc-types' ), array(), false );
$this->api->register_script( 'wc-entities', 'assets/client/blocks/wc-entities.js', array(), false );
$this->api->register_script( 'wc-blocks-middleware', 'assets/client/blocks/wc-blocks-middleware.js', array(), false );
$this->api->register_script( 'wc-blocks-data-store', 'assets/client/blocks/wc-blocks-data.js', array( 'wc-blocks-middleware' ) );
$this->api->register_script( 'wc-blocks-vendors', $this->api->get_block_asset_build_path( 'wc-blocks-vendors' ), array(), false );
$this->api->register_script( 'wc-blocks-registry', 'assets/client/blocks/wc-blocks-registry.js', array(), false );
$this->api->register_script( 'wc-blocks', $this->api->get_block_asset_build_path( 'wc-blocks' ), array( 'wc-blocks-vendors' ), false );
$this->api->register_script( 'wc-blocks-shared-context', 'assets/client/blocks/wc-blocks-shared-context.js' );
$this->api->register_script( 'wc-blocks-shared-hocs', 'assets/client/blocks/wc-blocks-shared-hocs.js', array(), false );
// The price package is shared externally so has no blocks prefix.
$this->api->register_script( 'wc-price-format', 'assets/client/blocks/price-format.js', array(), false );
// Vendor scripts for blocks frontends (not including cart and checkout).
$this->api->register_script( 'wc-blocks-frontend-vendors', $this->api->get_block_asset_build_path( 'wc-blocks-frontend-vendors-frontend' ), array(), true );
// Cart and checkout frontend scripts.
$this->api->register_script( 'wc-cart-checkout-vendors', $this->api->get_block_asset_build_path( 'wc-cart-checkout-vendors-frontend' ), array(), true );
$this->api->register_script( 'wc-cart-checkout-base', $this->api->get_block_asset_build_path( 'wc-cart-checkout-base-frontend' ), array(), true );
$this->api->register_script( 'wc-blocks-checkout', 'assets/client/blocks/blocks-checkout.js' );
$this->api->register_script( 'wc-blocks-checkout-events', 'assets/client/blocks/blocks-checkout-events.js' );
$this->api->register_script( 'wc-blocks-components', 'assets/client/blocks/blocks-components.js' );
$this->api->register_script( 'wc-schema-parser', 'assets/client/blocks/wc-schema-parser.js', array(), false );
// Sanitize.
$this->api->register_script(
'wc-sanitize',
'assets/client/admin/sanitize/index.js',
array()
);
// Customer Effort Score.
$this->api->register_script(
'wc-customer-effort-score',
'assets/client/admin/customer-effort-score/index.js',
array( 'wp-data', 'wp-data-controls', 'wc-store-data' )
);
$this->api->register_style(
'wc-customer-effort-score',
'assets/client/admin/customer-effort-score/style.css',
);
wp_add_inline_script(
'wc-blocks-middleware',
"
var wcBlocksMiddlewareConfig = {
storeApiNonce: '" . esc_js( wp_create_nonce( 'wc_store_api' ) ) . "',
wcStoreApiNonceTimestamp: '" . esc_js( time() ) . "'
};
",
'before'
);
}
/**
* Register and enqueue assets for exclusive usage within the Site Editor.
*/
public function register_and_enqueue_site_editor_assets() {
// Customer Effort Score.
wp_enqueue_script( 'wc-customer-effort-score' );
wp_enqueue_style( 'wc-customer-effort-score' );
}
/**
* Defines resource hints to help speed up the loading of some critical blocks.
*
* These will not impact page loading times negatively because they are loaded once the current page is idle.
*
* @param array $urls URLs to print for resource hints. Each URL is an array of resource attributes, or a URL string.
* @param string $relation_type The relation type the URLs are printed. Possible values: preconnect, dns-prefetch, prefetch, prerender.
* @return array URLs to print for resource hints.
*/
public function add_resource_hints( $urls, $relation_type ) {
if ( ! in_array( $relation_type, array( 'prefetch', 'prerender' ), true ) || is_admin() ) {
return $urls;
}
// We only need to prefetch when the cart has contents.
$cart = wc()->cart;
if ( ! $cart instanceof \WC_Cart || 0 === $cart->get_cart_contents_count() ) {
return $urls;
}
if ( 'prefetch' === $relation_type ) {
$urls = array_merge(
$urls,
$this->get_prefetch_resource_hints()
);
}
if ( 'prerender' === $relation_type ) {
$urls = array_merge(
$urls,
$this->get_prerender_resource_hints()
);
}
return $urls;
}
/**
* Get resource hints during prefetch requests.
*
* @return array Array of URLs.
*/
private function get_prefetch_resource_hints() {
$urls = array();
// Core page IDs.
$cart_page_id = wc_get_page_id( 'cart' );
$checkout_page_id = wc_get_page_id( 'checkout' );
// Checks a specific page (by ID) to see if it contains the named block.
$has_block_cart = $cart_page_id && has_block( 'woocommerce/cart', $cart_page_id );
$has_block_checkout = $checkout_page_id && has_block( 'woocommerce/checkout', $checkout_page_id );
// Checks the current page to see if it contains the named block.
$is_block_cart = has_block( 'woocommerce/cart' );
$is_block_checkout = has_block( 'woocommerce/checkout' );
if ( $has_block_cart && ! $is_block_cart ) {
$urls = array_merge( $urls, $this->get_block_asset_resource_hints( 'cart-frontend' ) );
}
if ( $has_block_checkout && ! $is_block_checkout ) {
$urls = array_merge( $urls, $this->get_block_asset_resource_hints( 'checkout-frontend' ) );
}
return $urls;
}
/**
* Get resource hints during prerender requests.
*
* @return array Array of URLs.
*/
private function get_prerender_resource_hints() {
$urls = array();
$is_block_cart = has_block( 'woocommerce/cart' );
if ( ! $is_block_cart ) {
return $urls;
}
$checkout_page_id = wc_get_page_id( 'checkout' );
$checkout_page_url = $checkout_page_id ? get_permalink( $checkout_page_id ) : '';
if ( $checkout_page_url ) {
$urls[] = $checkout_page_url;
}
return $urls;
}
/**
* Get the block asset resource hints in the cache or null if not found.
*
* @return array|null Array of resource hints.
*/
private function get_block_asset_resource_hints_cache() {
if ( wp_is_development_mode( 'plugin' ) ) {
return null;
}
$cache = get_transient( 'woocommerce_block_asset_resource_hints' );
$current_version = array(
'woocommerce' => Constants::get_constant( 'WC_VERSION' ),
'wordpress' => get_bloginfo( 'version' ),
'site_url' => site_url(),
);
if ( isset( $cache['version'] ) && $cache['version'] === $current_version ) {
return $cache['files'];
}
return null;
}
/**
* Set the block asset resource hints in the cache.
*
* @param string $filename File name.
* @param array $data Array of resource hints.
*/
private function set_block_asset_resource_hints_cache( $filename, $data ) {
$cache = $this->get_block_asset_resource_hints_cache();
$updated = array(
'files' => $cache ?? array(),
'version' => array(
'woocommerce' => Constants::get_constant( 'WC_VERSION' ),
'wordpress' => get_bloginfo( 'version' ),
'site_url' => site_url(),
),
);
$updated['files'][ $filename ] = $data;
set_transient( 'woocommerce_block_asset_resource_hints', $updated, WEEK_IN_SECONDS );
}
/**
* Get resource hint for a block by name.
*
* @param string $filename Block filename.
* @return array
*/
private function get_block_asset_resource_hints( $filename = '' ) {
if ( ! $filename ) {
return array();
}
$cached = $this->get_block_asset_resource_hints_cache();
if ( isset( $cached[ $filename ] ) ) {
return $cached[ $filename ];
}
$script_data = $this->api->get_script_data(
$this->api->get_block_asset_build_path( $filename )
);
$resources = array_merge(
array( esc_url( add_query_arg( 'ver', $script_data['version'], $script_data['src'] ) ) ),
$this->get_script_dependency_src_array( $script_data['dependencies'] )
);
$data = array_map(
function ( $src ) {
return array(
'href' => $src,
'as' => 'script',
);
},
array_unique( array_filter( $resources ) )
);
$this->set_block_asset_resource_hints_cache( $filename, $data );
return $data;
}
/**
* Get the src of all script dependencies (handles).
*
* @param array $dependencies Array of dependency handles.
* @return string[] Array of src strings.
*/
private function get_script_dependency_src_array( array $dependencies ) {
$wp_scripts = wp_scripts();
$found_dependencies = array();
$this->gather_script_dependency_handles( $dependencies, $wp_scripts, $found_dependencies );
$src = array();
foreach ( $found_dependencies as $handle => $unused ) {
$src[] = esc_url( add_query_arg( 'ver', $wp_scripts->registered[ $handle ]->ver, $this->get_absolute_url( $wp_scripts->registered[ $handle ]->src ) ) );
}
return $src;
}
/**
* Recursively gather all unique script dependency handles from a starting list.
*
* Traverses the dependency graph for each input handle, collecting any found handles
* and their nested dependencies in the provided array. Used internally to build a
* complete, deduplicated set of handles for further processing (e.g., mapping to src URLs).
*
* @param array $dependencies Array of initial script handles to process.
* @param \WP_Scripts $wp_scripts WP_Scripts instance containing all registered scripts.
* @param array $found_dependencies Reference to array in which discovered handles are stored.
*
* @return void
*/
private function gather_script_dependency_handles( array $dependencies, \WP_Scripts $wp_scripts, &$found_dependencies = array() ) {
foreach ( $dependencies as $handle ) {
if ( isset( $wp_scripts->registered[ $handle ] ) && ! isset( $found_dependencies[ $handle ] ) ) {
$found_dependencies[ $handle ] = true;
if ( ! empty( $wp_scripts->registered[ $handle ]->deps ) ) {
$this->gather_script_dependency_handles( $wp_scripts->registered[ $handle ]->deps, $wp_scripts, $found_dependencies );
}
}
}
}
/**
* Returns an absolute url to relative links for WordPress core scripts.
*
* @param string $src Original src that can be relative.
* @return string Correct full path string.
*/
private function get_absolute_url( $src ) {
$wp_scripts = wp_scripts();
if ( ! preg_match( '|^(https?:)?//|', $src ) && ! ( $wp_scripts->content_url && 0 === strpos( $src, $wp_scripts->content_url ) ) ) {
$src = $wp_scripts->base_url . $src;
}
return $src;
}
/**
* Skip Jetpack Boost minification on older versions of Jetpack Boost where it causes issues.
*
* @param mixed $do_concat Whether to concatenate the script or not.
* @param mixed $handle The script handle.
* @return mixed
*/
public function skip_boost_minification_for_cart_checkout( $do_concat, $handle ) {
$boost_is_outdated = defined( 'JETPACK_BOOST_VERSION' ) && version_compare( JETPACK_BOOST_VERSION, '3.4.2', '<' );
$scripts_to_ignore = array(
'wc-cart-checkout-vendors',
'wc-cart-checkout-base',
);
return $boost_is_outdated && in_array( $handle, $scripts_to_ignore, true ) ? false : $do_concat;
}
/**
* Add body classes to the frontend and within admin.
*
* @param string|array $classes Array or string of CSS classnames.
* @return string|array Modified classnames.
*/
public function add_theme_body_class( $classes ) {
$class = 'theme-' . get_template();
if ( is_array( $classes ) ) {
$classes[] = $class;
} else {
$classes .= ' ' . $class . ' ';
}
return $classes;
}
/**
* Get the file modified time as a cache buster if we're in dev mode.
*
* @param string $file Local path to the file.
* @return string The cache buster value to use for the given file.
*/
protected function get_file_version( $file ) {
if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG && file_exists( \Automattic\WooCommerce\Blocks\Package::get_path() . $file ) ) {
return filemtime( \Automattic\WooCommerce\Blocks\Package::get_path() . $file );
}
return $this->api->wc_version;
}
/**
* Registers a style according to `wp_register_style`.
*
* @param string $handle Name of the stylesheet. Should be unique.
* @param string $src Full URL of the stylesheet, or path of the stylesheet relative to the WordPress root directory.
* @param array $deps Optional. An array of registered stylesheet handles this stylesheet depends on. Default empty array.
* @param string $media Optional. The media for which this stylesheet has been defined. Default 'all'. Accepts media types like
* 'all', 'print' and 'screen', or media queries like '(orientation: portrait)' and '(max-width: 640px)'.
* @param boolean $rtl Optional. Whether or not to register RTL styles.
*/
protected function register_style( $handle, $src, $deps = array(), $media = 'all', $rtl = false ) {
$filename = str_replace( plugins_url( '/', dirname( __DIR__ ) ), '', $src );
$ver = self::get_file_version( $filename );
wp_register_style( $handle, $src, $deps, $ver, $media );
if ( $rtl ) {
wp_style_add_data( $handle, 'rtl', 'replace' );
}
}
/**
* Update block style dependencies after they have been registered.
*/
public function update_block_style_dependencies() {
$wp_styles = wp_styles();
$style = $wp_styles->query( 'wc-blocks-style', 'registered' );
if ( ! $style ) {
return;
}
// In WC < 5.5, `woocommerce-general` is not registered in block editor
// screens, so we don't add it as a dependency if it's not registered.
// In WC >= 5.5, `woocommerce-general` is registered on `admin_enqueue_scripts`,
// so we need to check if it's registered here instead of on `init`.
if (
wp_style_is( 'woocommerce-general', 'registered' ) &&
! in_array( 'woocommerce-general', $style->deps, true )
) {
$style->deps[] = 'woocommerce-general';
}
}
/**
* Fix scripts with wc-settings dependency.
*
* The wc-settings script only works correctly when enqueued in the footer. This is to give blocks etc time to
* register their settings data before it's printed.
*
* This code will look at registered scripts, and if they have a wc-settings dependency, force them to print in the
* footer instead of the header.
*
* This only supports packages known to require wc-settings!
*
* @see https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues/5052
*/
public function update_block_settings_dependencies() {
$wp_scripts = wp_scripts();
$known_packages = array( 'wc-settings', 'wc-blocks-checkout', 'wc-price-format' );
foreach ( $wp_scripts->registered as $handle => $script ) {
// scripts that are loaded in the footer has extra->group = 1.
if ( array_intersect( $known_packages, $script->deps ) && ! isset( $script->extra['group'] ) ) {
// Append the script to footer.
$wp_scripts->add_data( $handle, 'group', 1 );
// Show a warning.
$error_handle = 'wc-settings-dep-in-header';
$used_deps = implode( ', ', array_intersect( $known_packages, $script->deps ) );
$error_message = "Scripts that have a dependency on [$used_deps] must be loaded in the footer, {$handle} was registered to load in the header, but has been switched to load in the footer instead. See https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5059";
// phpcs:ignore WordPress.WP.EnqueuedResourceParameters.NotInFooter,WordPress.WP.EnqueuedResourceParameters.MissingVersion
wp_register_script( $error_handle, '' );
wp_enqueue_script( $error_handle );
wp_add_inline_script(
$error_handle,
sprintf( 'console.warn( "%s" );', $error_message )
);
}
}
}
/**
* Enqueue the wc-entities script.
*/
public function enqueue_wc_entities() {
wp_enqueue_script( 'wc-entities' );
}
}

View File

@@ -0,0 +1,280 @@
<?php
declare(strict_types=1);
namespace Automattic\WooCommerce\Blocks;
use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Blocks\Domain\Package;
use Automattic\WooCommerce\Blocks\Patterns\PatternRegistry;
use Automattic\WooCommerce\Blocks\Patterns\PTKPatternsStore;
/**
* Registers patterns under the `./patterns/` directory and from the PTK API and updates their content.
* Each pattern from core is defined as a PHP file and defines its metadata using plugin-style headers.
* The minimum required definition is:
*
* /**
* * Title: My Pattern
* * Slug: my-theme/my-pattern
* *
*
* The output of the PHP source corresponds to the content of the pattern, e.g.:
*
* <main><p><?php echo "Hello"; ?></p></main>
*
* Other settable fields include:
*
* - Description
* - Viewport Width
* - Categories (comma-separated values)
* - Keywords (comma-separated values)
* - Block Types (comma-separated values)
* - Inserter (yes/no)
*
* @internal
*/
class BlockPatterns {
const CATEGORIES_PREFIXES = [ '_woo_', '_dotcom_imported_' ];
/**
* Path to the patterns' directory.
*
* @var string $patterns_path
*/
private string $patterns_path;
/**
* PatternRegistry instance.
*
* @var PatternRegistry $pattern_registry
*/
private PatternRegistry $pattern_registry;
/**
* PTKPatternsStore instance.
*
* @var PTKPatternsStore $ptk_patterns_store
*/
private PTKPatternsStore $ptk_patterns_store;
/**
* Constructor for class
*
* @param Package $package An instance of Package.
* @param PatternRegistry $pattern_registry An instance of PatternRegistry.
* @param PTKPatternsStore $ptk_patterns_store An instance of PTKPatternsStore.
*/
public function __construct(
Package $package,
PatternRegistry $pattern_registry,
PTKPatternsStore $ptk_patterns_store
) {
$this->patterns_path = $package->get_path( 'patterns' );
$this->pattern_registry = $pattern_registry;
$this->ptk_patterns_store = $ptk_patterns_store;
add_action( 'init', array( $this, 'register_block_patterns' ) );
if ( Features::is_enabled( 'pattern-toolkit-full-composability' ) ) {
add_action( 'init', array( $this, 'register_ptk_patterns' ) );
}
}
/**
* Loads the content of a pattern.
*
* @param string $pattern_path The path to the pattern.
* @return string The content of the pattern.
*/
private function load_pattern_content( $pattern_path ) {
if ( ! file_exists( $pattern_path ) ) {
return '';
}
ob_start();
include $pattern_path;
return ob_get_clean();
}
/**
* Register block patterns from core.
*
* @return void
*/
public function register_block_patterns() {
if ( ! class_exists( 'WP_Block_Patterns_Registry' ) ) {
return;
}
$patterns = $this->get_block_patterns();
foreach ( $patterns as $pattern ) {
$pattern_path = $this->patterns_path . '/' . $pattern['source'];
$pattern['source'] = $pattern_path;
$content = $this->load_pattern_content( $pattern_path );
$pattern['content'] = $content;
$this->pattern_registry->register_block_pattern( $pattern_path, $pattern );
}
}
/**
* Gets block pattern data from the cache if available
*
* @return array Block pattern data.
*/
private function get_block_patterns() {
$pattern_data = $this->get_pattern_cache();
if ( is_array( $pattern_data ) ) {
return $pattern_data;
}
$default_headers = array(
'title' => 'Title',
'slug' => 'Slug',
'description' => 'Description',
'viewportWidth' => 'Viewport Width',
'categories' => 'Categories',
'keywords' => 'Keywords',
'blockTypes' => 'Block Types',
'inserter' => 'Inserter',
'featureFlag' => 'Feature Flag',
'templateTypes' => 'Template Types',
);
if ( ! file_exists( $this->patterns_path ) ) {
return array();
}
$files = glob( $this->patterns_path . '/*.php' );
if ( ! $files ) {
return array();
}
$patterns = array();
foreach ( $files as $file ) {
$data = get_file_data( $file, $default_headers );
// We want to store the relative path in the cache, so we can use it later to register the pattern.
$data['source'] = str_replace( $this->patterns_path . '/', '', $file );
$patterns[] = $data;
}
$this->set_pattern_cache( $patterns );
return $patterns;
}
/**
* Gets block pattern cache.
*
* @return array|false Returns an array of patterns if cache is found, otherwise false.
*/
private function get_pattern_cache() {
$pattern_data = get_site_transient( 'woocommerce_blocks_patterns' );
if ( is_array( $pattern_data ) && WOOCOMMERCE_VERSION === $pattern_data['version'] ) {
return $pattern_data['patterns'];
}
return false;
}
/**
* Sets block pattern cache.
*
* @param array $patterns Block patterns data to set in cache.
*/
private function set_pattern_cache( array $patterns ) {
$pattern_data = array(
'version' => WOOCOMMERCE_VERSION,
'patterns' => $patterns,
);
set_site_transient( 'woocommerce_blocks_patterns', $pattern_data, MONTH_IN_SECONDS );
}
/**
* Register patterns from the Patterns Toolkit.
*
* @return void
*/
public function register_ptk_patterns() {
// Only if the user has allowed tracking, we register the patterns from the PTK.
$allow_tracking = 'yes' === get_option( 'woocommerce_allow_tracking' );
if ( ! $allow_tracking ) {
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 ) || ! is_array( $patterns ) ) {
// Only log once per day by using a transient.
$transient_key = 'wc_ptk_pattern_store_warning';
// 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 ( ! get_transient( $transient_key ) && ! call_user_func( $has_scheduled_action, 'fetch_patterns' ) ) {
wc_get_logger()->warning(
__( 'Empty patterns received from the PTK Pattern Store', 'woocommerce' ),
);
// Set the transient to true to indicate that the warning has been logged in the current day.
set_transient( $transient_key, true, DAY_IN_SECONDS );
}
return;
}
$patterns = $this->parse_categories( $patterns );
foreach ( $patterns as $pattern ) {
$pattern['slug'] = $pattern['name'];
$pattern['content'] = $pattern['html'];
$this->pattern_registry->register_block_pattern( $pattern['ID'], $pattern );
}
}
/**
* Parse prefixed categories from the PTK patterns into the actual WooCommerce categories.
*
* @param array $patterns The patterns to parse.
* @return array The parsed patterns.
*/
private function parse_categories( array $patterns ) {
return array_map(
function ( $pattern ) {
if ( ! isset( $pattern['categories'] ) ) {
$pattern['categories'] = array();
}
$values = array_values( $pattern['categories'] );
foreach ( $values as $value ) {
if ( ! isset( $value['title'] ) || ! isset( $value['slug'] ) ) {
$pattern['categories'] = array();
}
}
$pattern['categories'] = array_map(
function ( $category ) {
foreach ( self::CATEGORIES_PREFIXES as $prefix ) {
if ( strpos( $category['title'], $prefix ) !== false ) {
$parsed_category = str_replace( $prefix, '', $category['title'] );
$parsed_category = str_replace( '_', ' ', $parsed_category );
$category['title'] = ucfirst( $parsed_category );
}
}
return $category;
},
$pattern['categories']
);
return $pattern;
},
$patterns
);
}
}

View File

@@ -0,0 +1,434 @@
<?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\Blocks;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
use Automattic\WooCommerce\Blocks\Templates\ComingSoonTemplate;
use Automattic\WooCommerce\Blocks\Utils\Utils;
/**
* BlockTemplatesController class.
*
* @internal
*/
class BlockTemplatesController {
/**
* Directory which contains all templates
*
* @var string
*/
const TEMPLATES_ROOT_DIR = 'templates';
/**
* Initialization method.
*/
public function init() {
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_db_templates_with_woo_slug' ), 10, 3 );
add_filter( 'rest_pre_insert_wp_template', array( $this, 'dont_load_templates_for_suggestions' ), 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 );
add_action( 'wp_enqueue_scripts', [ $this, 'dequeue_legacy_scripts' ], 20 );
// Fix a bug in WordPress 6.8 and lower that caused block hooks not to
// run in templates registered via the Template Registration API.
// @see https://github.com/WordPress/gutenberg/issues/71139.
if ( Utils::wp_version_compare( '6.8', '<=' ) ) {
add_filter( 'get_block_templates', array( $this, 'run_hooks_on_block_templates' ), 10, 3 );
}
}
/**
* Dequeue legacy scripts that have no usage with block themes.
*/
public function dequeue_legacy_scripts() {
if ( ! wp_is_block_theme() ) {
return;
}
if ( is_product() ) {
wp_dequeue_script( 'wc-single-product' );
}
}
/**
* Renders the `core/template-part` block on the server.
*
* This is done because the core handling for template parts only supports templates from the current theme, not
* from a plugin.
*
* @param array $attributes The block attributes.
* @return string The render.
*/
public function render_woocommerce_template_part( $attributes ) {
if ( isset( $attributes['theme'] ) && 'woocommerce/woocommerce' === $attributes['theme'] ) {
$template_part = get_block_template( $attributes['theme'] . '//' . $attributes['slug'], 'wp_template_part' );
if ( $template_part && ! empty( $template_part->content ) ) {
$content = do_blocks( $template_part->content );
if ( empty( $attributes['tagName'] ) || tag_escape( $attributes['tagName'] ) !== $attributes['tagName'] ) {
$html_tag = 'div';
} else {
$html_tag = esc_attr( $attributes['tagName'] );
}
$wrapper_attributes = get_block_wrapper_attributes();
return "<$html_tag $wrapper_attributes>" . str_replace( ']]>', ']]&gt;', $content ) . "</$html_tag>";
}
}
return function_exists( '\gutenberg_render_block_core_template_part' ) ? \gutenberg_render_block_core_template_part( $attributes ) : \render_block_core_template_part( $attributes );
}
/**
* By default, the Template Part Block only supports template parts that are in the current theme directory.
* This render_callback wrapper allows us to add support for plugin-housed template parts.
*
* @param array $settings Array of determined settings for registering a block type.
* @param array $metadata Metadata provided for registering a block type.
*/
public function add_plugin_templates_parts_support( $settings, $metadata ) {
if (
isset( $metadata['name'], $settings['render_callback'] ) &&
'core/template-part' === $metadata['name'] &&
in_array( $settings['render_callback'], array( 'render_block_core_template_part', 'gutenberg_render_block_core_template_part' ), true )
) {
$settings['render_callback'] = array( $this, 'render_woocommerce_template_part' );
}
return $settings;
}
/**
* Prevents shortcodes in templates having their HTML content broken by wpautop.
*
* @see https://core.trac.wordpress.org/ticket/58366 for more info.
*
* @param array $settings Array of determined settings for registering a block type.
* @param array $metadata Metadata provided for registering a block type.
*/
public function prevent_shortcodes_html_breakage( $settings, $metadata ) {
if (
isset( $metadata['name'], $settings['render_callback'] ) &&
'core/shortcode' === $metadata['name']
) {
$settings['original_render_callback'] = $settings['render_callback'];
$settings['render_callback'] = function ( $attributes, $content ) use ( $settings ) {
// The shortcode has already been rendered, so look for the cart/checkout HTML.
if ( strstr( $content, 'woocommerce-cart-form' ) || strstr( $content, 'wc-empty-cart-message' ) || strstr( $content, 'woocommerce-checkout-form' ) ) {
// Return early before wpautop runs again.
return $content;
}
$render_callback = $settings['original_render_callback'];
return $render_callback( $attributes, $content );
};
}
return $settings;
}
/**
* Prevents the pages that are assigned as Cart/Checkout from showing the "template" selector in the page-editor.
* We want to avoid this flow and point users towards the Site Editor instead.
*
* @return void
*/
public function hide_template_selector_in_cart_checkout_pages() {
if ( ! is_admin() ) {
return;
}
$current_screen = get_current_screen();
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( $current_screen && 'page' === $current_screen->id && ! empty( $_GET['post'] ) && in_array( absint( $_GET['post'] ), array( wc_get_page_id( 'cart' ), wc_get_page_id( 'checkout' ) ), true ) ) {
wp_add_inline_style( 'wc-blocks-editor-style', '.edit-post-post-template { display: none; }' );
}
}
/**
* This function checks if there's a block template file in `woocommerce/templates/templates/`
* to return to pre_get_posts short-circuiting the query in Gutenberg.
*
* @param \WP_Block_Template|null $template Return a 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).
* @param string $template_type wp_template or wp_template_part.
*
* @return mixed|\WP_Block_Template|\WP_Error
*/
public function get_block_file_template( $template, $id, $template_type ) {
$template_name_parts = explode( '//', $id );
if ( count( $template_name_parts ) < 2 ) {
return $template;
}
list( $template_id, $template_slug ) = $template_name_parts;
// This is a real edge-case, we are supporting users who have saved templates under the deprecated slug. See its definition for more information.
// You can likely ignore this code unless you're supporting/debugging early customised templates.
if ( BlockTemplateUtils::DEPRECATED_PLUGIN_SLUG === strtolower( $template_id ) ) {
// Because we are using get_block_templates we have to unhook this method to prevent a recursive loop where this filter is applied.
remove_filter( 'pre_get_block_file_template', array( $this, 'get_block_file_template' ), 10, 3 );
$template_with_deprecated_id = get_block_template( $id, $template_type );
// Let's hook this method back now that we have used the function.
add_filter( 'pre_get_block_file_template', array( $this, 'get_block_file_template' ), 10, 3 );
if ( null !== $template_with_deprecated_id ) {
return $template_with_deprecated_id;
}
}
// If we are not dealing with a WooCommerce template let's return early and let it continue through the process.
if ( BlockTemplateUtils::PLUGIN_SLUG !== $template_id ) {
return $template;
}
// If we don't have a template let Gutenberg do its thing.
if ( ! $this->block_template_is_available( $template_slug, $template_type ) ) {
return $template;
}
$directory = BlockTemplateUtils::get_templates_directory( $template_type );
$template_file_path = $directory . '/' . $template_slug . '.html';
$template_object = BlockTemplateUtils::create_new_block_template_object( $template_file_path, $template_type, $template_slug );
$template_built = BlockTemplateUtils::build_template_result_from_file( $template_object, $template_type );
if ( null !== $template_built ) {
return $template_built;
}
// Hand back over to Gutenberg if we can't find a template.
return $template;
}
/**
* Add the template title and description to WooCommerce templates.
*
* @param WP_Block_Template|null $block_template The found block template, or null if there isn't one.
* @param string $id Template unique identifier (example: 'theme_slug//template_slug').
* @param array $template_type Template type: 'wp_template' or 'wp_template_part'.
* @return WP_Block_Template|null
*/
public function add_block_template_details( $block_template, $id, $template_type ) {
return BlockTemplateUtils::update_template_data( $block_template, $template_type );
}
/**
* Run hooks on block templates.
*
* @param array $templates The block templates.
* @return array The block templates.
*/
public function run_hooks_on_block_templates( $templates ) {
foreach ( $templates as $template ) {
if ( 'plugin' === $template->source && 'woocommerce' === $template->plugin ) {
$template->content = apply_block_hooks_to_content( $template->content, $template, 'insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata' );
}
}
return $templates;
}
/**
* Add the block template objects currently saved in the database with the WooCommerce slug.
* That is, templates that have been customised before WooCommerce started to use the
* Template Registration API.
*
* @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.
* @return array
*/
public function add_db_templates_with_woo_slug( $query_result, $query, $template_type ) {
$slugs = isset( $query['slug__in'] ) ? $query['slug__in'] : array();
if ( ! BlockTemplateUtils::supports_block_templates( $template_type ) && ! in_array( ComingSoonTemplate::SLUG, $slugs, true ) ) {
return $query_result;
}
// For templates, we only need to load templates from the database. For
// template parts, we also need to load them from the filesystem, as
// there is no Template registration API for template parts.
$template_files = 'wp_template' === $template_type ? BlockTemplateUtils::get_block_templates_from_db( $slugs, $template_type ) : $this->get_block_templates( $slugs, $template_type );
$new_templates = array();
foreach ( $template_files as $template_file ) {
// It would be custom if the template was modified in the editor, so if it's not custom we can load it from
// the filesystem.
if ( 'custom' === $template_file->source ) {
if (
BlockTemplateUtils::PLUGIN_SLUG === $template_file->theme ||
BlockTemplateUtils::DEPRECATED_PLUGIN_SLUG === $template_file->theme
) {
array_unshift( $new_templates, $template_file );
}
continue;
}
// We only need to build templates from the filesystem for template parts.
// Regular templates are handled by the Template registration API.
if ( 'wp_template_part' === $template_type ) {
$theme_slug = get_stylesheet();
$possible_template_ids = [
$theme_slug . '//' . $template_file->slug,
$theme_slug . '//' . BlockTemplateUtils::DIRECTORY_NAMES['TEMPLATE_PARTS'] . '/' . $template_file->slug,
$theme_slug . '//' . BlockTemplateUtils::DIRECTORY_NAMES['DEPRECATED_TEMPLATE_PARTS'] . '/' . $template_file->slug,
];
$is_custom = false;
$query_result_template_ids = array_column( $query_result, 'id' );
foreach ( $possible_template_ids as $template_id ) {
if ( in_array( $template_id, $query_result_template_ids, true ) ) {
$is_custom = true;
break;
}
}
$fits_slug_query =
! isset( $query['slug__in'] ) || in_array( $template_file->slug, $query['slug__in'], true );
$fits_area_query =
! isset( $query['area'] ) || ( property_exists( $template_file, 'area' ) && $template_file->area === $query['area'] );
$is_from_filesystem = isset( $template_file->path );
$should_include = ! $is_custom && $fits_slug_query && $fits_area_query && $is_from_filesystem;
if ( $should_include ) {
$template = BlockTemplateUtils::build_template_result_from_file( $template_file, $template_type );
$query_result[] = $template;
}
}
}
$query_result = array_merge( $new_templates, $query_result );
if ( count( $new_templates ) > 0 ) {
// If there are certain templates that have been customised with the `woocommerce/woocommerce` slug,
// We prioritize them over the theme and WC templates. That is, we remove the theme and WC templates
// from the results and only keep the customised ones.
$query_result = BlockTemplateUtils::remove_templates_with_custom_alternative( $query_result );
// There is the chance that the user customized the default template, installed a theme with a custom template
// and customized that one as well. When that happens, duplicates might appear in the list.
// See: https://github.com/woocommerce/woocommerce/issues/42220.
$query_result = BlockTemplateUtils::remove_duplicate_customized_templates( $query_result );
}
/**
* WC templates from theme aren't included in `$this->get_block_templates()` but are handled by Gutenberg.
* We need to do additional search through all templates file to update title and description for WC
* templates that aren't listed in theme.json.
*/
$query_result = array_map(
function ( $template ) use ( $template_type ) {
return BlockTemplateUtils::update_template_data( $template, $template_type );
},
$query_result
);
return $query_result;
}
/**
* When creating a template from the WP suggestion, don't load the templates with the WooCommerce slug.
* Otherwise they take precedence and the new template can't be created.
*
* @param stdClass $prepared_post An object representing a single post prepared
* for inserting or updating the database.
*/
public function dont_load_templates_for_suggestions( $prepared_post ) {
if ( isset( $prepared_post->meta_input['is_wp_suggestion'] ) ) {
remove_filter( 'get_block_templates', array( $this, 'add_db_templates_with_woo_slug' ), 10, 3 );
}
return $prepared_post;
}
/**
* Gets the templates from the WooCommerce blocks directory, skipping those for which a template already exists
* in the theme directory.
*
* @param string[] $slugs An array of slugs to filter templates by. Templates whose slug does not match will not be returned.
* @param array $already_found_templates Templates that have already been found, these are customised templates that are loaded from the database.
* @param string $template_type wp_template or wp_template_part.
*
* @return array Templates from the WooCommerce blocks plugin directory.
*/
public function get_block_templates_from_woocommerce( $slugs, $already_found_templates, $template_type = 'wp_template' ) {
$template_files = BlockTemplateUtils::get_template_paths( $template_type );
$templates = array();
foreach ( $template_files as $template_file ) {
// Skip the template if it's blockified, and we should only use classic ones.
if ( ! BlockTemplateUtils::should_use_blockified_product_grid_templates() && strpos( $template_file, 'blockified' ) !== false ) {
continue;
}
$template_slug = BlockTemplateUtils::generate_template_slug_from_path( $template_file );
// This template does not have a slug we're looking for. Skip it.
if ( is_array( $slugs ) && count( $slugs ) > 0 && ! in_array( $template_slug, $slugs, true ) ) {
continue;
}
// If the theme already has a template, or the template is already in the list (i.e. it came from the
// database) then we should not overwrite it with the one from the filesystem.
if (
BlockTemplateUtils::theme_has_template( $template_slug ) ||
count(
array_filter(
$already_found_templates,
function ( $template ) use ( $template_slug ) {
$template_obj = (object) $template; //phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.Found
return $template_obj->slug === $template_slug;
}
)
) > 0 ) {
continue;
}
// At this point the template only exists in the Blocks filesystem and has not been saved in the DB,
// or superseded by the theme.
$templates[] = BlockTemplateUtils::create_new_block_template_object( $template_file, $template_type, $template_slug );
}
return $templates;
}
/**
* Get and build the block template objects from the block template files.
*
* @param array $slugs An array of slugs to retrieve templates for.
* @param string $template_type wp_template or wp_template_part.
*
* @return array WP_Block_Template[] An array of block template objects.
*/
public function get_block_templates( $slugs = array(), $template_type = 'wp_template' ) {
$templates_from_db = BlockTemplateUtils::get_block_templates_from_db( $slugs, $template_type );
$templates_from_woo = $this->get_block_templates_from_woocommerce( $slugs, $templates_from_db, $template_type );
return array_merge( $templates_from_db, $templates_from_woo );
}
/**
* Checks whether a block template with that name exists in Woo Blocks
*
* @param string $template_name Template to check.
* @param array $template_type wp_template or wp_template_part.
*
* @return boolean
*/
public function block_template_is_available( $template_name, $template_type = 'wp_template' ) {
if ( ! $template_name ) {
return false;
}
$directory = BlockTemplateUtils::get_templates_directory( $template_type ) . '/' . $template_name . '.html';
return is_readable(
$directory
) || $this->get_block_templates( array( $template_name ), $template_type );
}
}

View File

@@ -0,0 +1,151 @@
<?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\Blocks;
use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
use Automattic\WooCommerce\Blocks\Templates\AbstractTemplate;
use Automattic\WooCommerce\Blocks\Templates\AbstractTemplatePart;
use Automattic\WooCommerce\Blocks\Templates\MiniCartTemplate;
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\OrderConfirmationTemplate;
use Automattic\WooCommerce\Blocks\Templates\ProductAttributeTemplate;
use Automattic\WooCommerce\Blocks\Templates\ProductBrandTemplate;
use Automattic\WooCommerce\Blocks\Templates\ProductCatalogTemplate;
use Automattic\WooCommerce\Blocks\Templates\ProductCategoryTemplate;
use Automattic\WooCommerce\Blocks\Templates\ProductTagTemplate;
use Automattic\WooCommerce\Blocks\Templates\ProductSearchResultsTemplate;
use Automattic\WooCommerce\Blocks\Templates\SingleProductTemplate;
use Automattic\WooCommerce\Blocks\Templates\SimpleProductAddToCartWithOptionsTemplate;
use Automattic\WooCommerce\Blocks\Templates\ExternalProductAddToCartWithOptionsTemplate;
use Automattic\WooCommerce\Blocks\Templates\VariableProductAddToCartWithOptionsTemplate;
use Automattic\WooCommerce\Blocks\Templates\GroupedProductAddToCartWithOptionsTemplate;
use Automattic\WooCommerce\Enums\ProductType;
/**
* BlockTemplatesRegistry class.
*
* @internal
*/
class BlockTemplatesRegistry {
/**
* The array of registered templates.
*
* @var AbstractTemplate[]|AbstractTemplatePart[]
*/
private $templates = array();
/**
* Initialization method.
*/
public function init() {
if ( BlockTemplateUtils::supports_block_templates( 'wp_template' ) ) {
$templates = array(
ProductCatalogTemplate::SLUG => new ProductCatalogTemplate(),
ProductCategoryTemplate::SLUG => new ProductCategoryTemplate(),
ProductTagTemplate::SLUG => new ProductTagTemplate(),
ProductAttributeTemplate::SLUG => new ProductAttributeTemplate(),
ProductBrandTemplate::SLUG => new ProductBrandTemplate(),
ProductSearchResultsTemplate::SLUG => new ProductSearchResultsTemplate(),
CartTemplate::SLUG => new CartTemplate(),
CheckoutTemplate::SLUG => new CheckoutTemplate(),
OrderConfirmationTemplate::SLUG => new OrderConfirmationTemplate(),
SingleProductTemplate::SLUG => new SingleProductTemplate(),
);
} else {
$templates = array();
}
if ( Features::is_enabled( 'launch-your-store' ) ) {
$templates[ ComingSoonTemplate::SLUG ] = new ComingSoonTemplate();
}
if ( BlockTemplateUtils::supports_block_templates( 'wp_template_part' ) ) {
$template_parts = array(
MiniCartTemplate::SLUG => new MiniCartTemplate(),
CheckoutHeaderTemplate::SLUG => new CheckoutHeaderTemplate(),
);
if ( wp_is_block_theme() ) {
$product_types = wc_get_product_types();
if ( count( $product_types ) > 0 ) {
add_filter( 'default_wp_template_part_areas', array( $this, 'register_add_to_cart_with_options_template_part_area' ), 10, 1 );
if ( array_key_exists( ProductType::SIMPLE, $product_types ) ) {
$template_parts[ SimpleProductAddToCartWithOptionsTemplate::SLUG ] = new SimpleProductAddToCartWithOptionsTemplate();
}
if ( array_key_exists( ProductType::EXTERNAL, $product_types ) ) {
$template_parts[ ExternalProductAddToCartWithOptionsTemplate::SLUG ] = new ExternalProductAddToCartWithOptionsTemplate();
}
if ( array_key_exists( ProductType::VARIABLE, $product_types ) ) {
$template_parts[ VariableProductAddToCartWithOptionsTemplate::SLUG ] = new VariableProductAddToCartWithOptionsTemplate();
}
if ( array_key_exists( ProductType::GROUPED, $product_types ) ) {
$template_parts[ GroupedProductAddToCartWithOptionsTemplate::SLUG ] = new GroupedProductAddToCartWithOptionsTemplate();
}
}
}
} else {
$template_parts = array();
}
// Init all templates.
foreach ( $templates as $template ) {
$template->init();
// Taxonomy templates are registered automatically by WordPress and
// are made available through the Add Template menu.
if ( ! $template->is_taxonomy_template ) {
$directory = BlockTemplateUtils::get_templates_directory( 'wp_template' );
$template_file_path = $directory . '/' . $template::SLUG . '.html';
register_block_template(
'woocommerce//' . $template::SLUG,
array(
'title' => $template->get_template_title(),
'description' => $template->get_template_description(),
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
'content' => file_get_contents( $template_file_path ),
)
);
}
}
foreach ( $template_parts as $template_part ) {
$template_part->init();
}
$this->templates = array_merge( $templates, $template_parts );
}
/**
* Add Add to Cart + Options to the default template part areas.
*
* @param array $default_area_definitions An array of supported area objects.
* @return array The supported template part areas including the Add to Cart + Options one.
*/
public function register_add_to_cart_with_options_template_part_area( $default_area_definitions ) {
$add_to_cart_with_options_template_part_area = array(
'area' => 'add-to-cart-with-options',
'label' => __( 'Add to Cart + Options', 'woocommerce' ),
'description' => __( 'The Add to Cart + Options templates allow defining a different layout for each product type.', 'woocommerce' ),
'icon' => 'add-to-cart-with-options',
'area_tag' => 'add-to-cart-with-options',
);
return array_merge( $default_area_definitions, array( $add_to_cart_with_options_template_part_area ) );
}
/**
* Returns the template matching the slug
*
* @param string $template_slug Slug of the template to retrieve.
*
* @return AbstractTemplate|AbstractTemplatePart|null
*/
public function get_template( $template_slug ) {
if ( array_key_exists( $template_slug, $this->templates ) ) {
$registered_template = $this->templates[ $template_slug ];
return $registered_template;
}
return null;
}
}

View File

@@ -0,0 +1,515 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use WP_Block;
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
use Automattic\WooCommerce\Blocks\Assets\Api as AssetApi;
use Automattic\WooCommerce\Blocks\Integrations\IntegrationRegistry;
use Automattic\WooCommerce\Admin\Features\Features;
/**
* AbstractBlock class.
*/
abstract class AbstractBlock {
/**
* Block namespace.
*
* @var string
*/
protected $namespace = 'woocommerce';
/**
* Block name within this namespace.
*
* @var string
*/
protected $block_name = '';
/**
* Tracks if assets have been enqueued.
*
* @var boolean
*/
protected $enqueued_assets = false;
/**
* Instance of the asset API.
*
* @var AssetApi
*/
protected $asset_api;
/**
* Instance of the asset data registry.
*
* @var AssetDataRegistry
*/
protected $asset_data_registry;
/**
* Instance of the integration registry.
*
* @var IntegrationRegistry
*/
protected $integration_registry;
/**
* Constructor.
*
* @param AssetApi $asset_api Instance of the asset API.
* @param AssetDataRegistry $asset_data_registry Instance of the asset data registry.
* @param IntegrationRegistry $integration_registry Instance of the integration registry.
* @param string $block_name Optionally set block name during construct.
*/
public function __construct( AssetApi $asset_api, AssetDataRegistry $asset_data_registry, IntegrationRegistry $integration_registry, $block_name = '' ) {
$this->asset_api = $asset_api;
$this->asset_data_registry = $asset_data_registry;
$this->integration_registry = $integration_registry;
$this->block_name = $block_name ? $block_name : $this->block_name;
$this->initialize();
}
/**
* Get the interactivity namespace. Only used when utilizing the interactivity API.
* @return string The interactivity namespace, used to namespace interactivity API actions and state.
*/
protected function get_full_block_name() {
return $this->namespace . '/' . $this->block_name;
}
/**
* The default render_callback for all blocks. This will ensure assets are enqueued just in time, then render
* the block (if applicable).
*
* @param array|WP_Block $attributes Block attributes, or an instance of a WP_Block. Defaults to an empty array.
* @param string $content Block content. Default empty string.
* @param WP_Block|null $block Block instance.
* @return string Rendered block type output.
*/
public function render_callback( $attributes = [], $content = '', $block = null ) {
$render_callback_attributes = $this->parse_render_callback_attributes( $attributes );
if ( ! is_admin() && ! WC()->is_rest_api_request() ) {
$this->register_block_type_assets();
$this->enqueue_assets( $render_callback_attributes, $content, $block );
}
return $this->render( $render_callback_attributes, $content, $block );
}
/**
* Enqueue assets used for rendering the block in editor context.
*
* This is needed if a block is not yet within the post content--`render` and `enqueue_assets` may not have ran.
*/
public function enqueue_editor_assets() {
if ( $this->enqueued_assets ) {
return;
}
$this->register_block_type_assets();
$this->enqueue_data();
}
/**
* Are we currently on the admin block editor screen?
*/
protected function is_block_editor() {
if ( ! is_admin() || ! function_exists( 'get_current_screen' ) ) {
return false;
}
$screen = get_current_screen();
return $screen && $screen->is_block_editor();
}
/**
* Initialize this block type.
*
* - Hook into WP lifecycle.
* - Register the block with WordPress.
*/
protected function initialize() {
if ( empty( $this->block_name ) ) {
_doing_it_wrong( __METHOD__, esc_html__( 'Block name is required.', 'woocommerce' ), '4.5.0' );
return false;
}
$this->integration_registry->initialize( $this->block_name . '_block' );
$this->register_block_type();
add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_editor_assets' ] );
}
/**
* Register script and style assets for the block type before it is registered.
*
* This registers the scripts; it does not enqueue them.
*/
protected function register_block_type_assets() {
if ( null !== $this->get_block_type_editor_script() ) {
$data = $this->asset_api->get_script_data( $this->get_block_type_editor_script( 'path' ) );
$has_i18n = in_array( 'wp-i18n', $data['dependencies'], true );
$this->asset_api->register_script(
$this->get_block_type_editor_script( 'handle' ),
$this->get_block_type_editor_script( 'path' ),
array_merge(
$this->get_block_type_editor_script( 'dependencies' ),
$this->integration_registry->get_all_registered_editor_script_handles()
),
$has_i18n
);
}
if ( null !== $this->get_block_type_script() ) {
$data = $this->asset_api->get_script_data( $this->get_block_type_script( 'path' ) );
$has_i18n = in_array( 'wp-i18n', $data['dependencies'], true );
$this->asset_api->register_script(
$this->get_block_type_script( 'handle' ),
$this->get_block_type_script( 'path' ),
array_merge(
$this->get_block_type_script( 'dependencies' ),
$this->integration_registry->get_all_registered_script_handles()
),
$has_i18n
);
}
}
/**
* Injects Chunk Translations into the page so translations work for lazy loaded components.
*
* The chunk names are defined when creating lazy loaded components using webpackChunkName.
*
* @param string[] $chunks Array of chunk names.
*/
protected function register_chunk_translations( $chunks ) {
foreach ( $chunks as $chunk ) {
$handle = 'wc-blocks-' . $chunk . '-chunk';
$this->asset_api->register_script( $handle, $this->asset_api->get_block_asset_build_path( $chunk ), [], true );
wp_add_inline_script(
$this->get_block_type_script( 'handle' ),
wp_scripts()->print_translations( $handle, false ),
'before'
);
wp_deregister_script( $handle );
}
}
/**
* Generate an array of chunks paths for loading translation.
*
* @param string $chunks_folder The folder to iterate over.
* @return string[] $chunks list of chunks to load.
*/
protected function get_chunks_paths( $chunks_folder ) {
$build_path = \Automattic\WooCommerce\Blocks\Package::get_path() . 'assets/client/blocks/';
$blocks = [];
if ( ! is_dir( $build_path . $chunks_folder ) ) {
return [];
}
foreach ( new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $build_path . $chunks_folder, \FilesystemIterator::UNIX_PATHS ) ) as $block_name ) {
$blocks[] = str_replace( $build_path, '', $block_name );
}
$chunks = preg_filter( '/.js/', '', $blocks );
return $chunks;
}
/**
* Registers the block type with WordPress.
*
* @return string[] Chunks paths.
*/
protected function register_block_type() {
$block_settings = [
'render_callback' => $this->get_block_type_render_callback(),
'editor_script' => $this->get_block_type_editor_script( 'handle' ),
];
// Conditionally override these, otherwise rely on block.json metadata.
if ( $this->get_block_type_style() ) {
$block_settings['style'] = $this->get_block_type_style();
}
if ( $this->get_block_type_editor_style() ) {
$block_settings['editor_style'] = $this->get_block_type_editor_style();
}
if ( isset( $this->api_version ) ) {
$block_settings['api_version'] = intval( $this->api_version );
}
$metadata_path = $this->asset_api->get_block_metadata_path( $this->block_name );
// Prefer to register with metadata if the path is set in the block's class.
if ( ! empty( $metadata_path ) ) {
register_block_type_from_metadata(
$metadata_path,
$block_settings
);
return;
}
/*
* Insert attributes and supports if we're not registering the block using metadata.
* These are left unset until now and only added here because if they were set when registering with metadata,
* the attributes and supports from $block_settings would override the values from metadata.
*/
$block_settings['attributes'] = $this->get_block_type_attributes();
$block_settings['supports'] = $this->get_block_type_supports();
$block_settings['uses_context'] = $this->get_block_type_uses_context();
register_block_type(
$this->get_block_type(),
$block_settings
);
}
/**
* Get the block type.
*
* @return string
*/
protected function get_block_type() {
return $this->namespace . '/' . $this->block_name;
}
/**
* Get the render callback for this block type.
*
* Dynamic blocks should return a callback, for example, `return [ $this, 'render' ];`
*
* @see $this->register_block_type()
* @return callable|null;
*/
protected function get_block_type_render_callback() {
return [ $this, 'render_callback' ];
}
/**
* Get the editor script data 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_editor_script( $key = null ) {
$script = [
'handle' => 'wc-' . $this->block_name . '-block',
'path' => $this->asset_api->get_block_asset_build_path( $this->block_name ),
'dependencies' => [ 'wc-blocks' ],
];
return $key ? $script[ $key ] : $script;
}
/**
* Get the editor style handle for this block type.
*
* @see $this->register_block_type()
* @return string|null
*/
protected function get_block_type_editor_style() {
return 'wc-blocks-editor-style';
}
/**
* 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 ) {
$script = [
'handle' => 'wc-' . $this->block_name . '-block-frontend',
'path' => $this->asset_api->get_block_asset_build_path( $this->block_name . '-frontend' ),
'dependencies' => [],
];
return $key ? $script[ $key ] : $script;
}
/**
* Get the frontend style handle for this block type.
*
* @return string[]|null
*/
protected function get_block_type_style() {
$this->asset_api->register_style( 'wc-blocks-style-' . $this->block_name, $this->asset_api->get_block_asset_build_path( $this->block_name, 'css' ), [], 'all', true );
return [ 'wc-blocks-style', 'wc-blocks-style-' . $this->block_name ];
}
/**
* Get the supports array for this block type.
*
* @see $this->register_block_type()
* @return string;
*/
protected function get_block_type_supports() {
return [];
}
/**
* Get block attributes.
*
* @return array;
*/
protected function get_block_type_attributes() {
return [];
}
/**
* Get block usesContext.
*
* @return array;
*/
protected function get_block_type_uses_context() {
return [];
}
/**
* Parses block attributes from the render_callback.
*
* @param array|WP_Block $attributes Block attributes, or an instance of a WP_Block. Defaults to an empty array.
* @return array
*/
protected function parse_render_callback_attributes( $attributes ) {
return is_a( $attributes, 'WP_Block' ) ? $attributes->attributes : $attributes;
}
/**
* Render the block. Extended by children.
*
* @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 ) {
return $content;
}
/**
* Enqueue frontend assets for this block, just in time for rendering.
*
* @internal This prevents the block script being enqueued on all pages. It is only enqueued as needed. Note that
* we intentionally do not pass 'script' to register_block_type.
*
* @param array $attributes Any attributes that currently are available from the block.
* @param string $content The block content.
* @param WP_Block $block The block object.
*/
protected function enqueue_assets( array $attributes, $content, $block ) {
if ( $this->enqueued_assets ) {
return;
}
$this->enqueue_data( $attributes );
$this->enqueue_scripts( $attributes );
$this->enqueued_assets = true;
}
/**
* 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 = [] ) {
$registered_script_data = $this->integration_registry->get_all_registered_script_data();
foreach ( $registered_script_data as $asset_data_key => $asset_data_value ) {
if ( ! $this->asset_data_registry->exists( $asset_data_key ) ) {
$this->asset_data_registry->add( $asset_data_key, $asset_data_value );
}
}
if ( ! $this->asset_data_registry->exists( 'wcBlocksConfig' ) ) {
$wc_blocks_config = [
'pluginUrl' => plugins_url( '/', dirname( __DIR__, 2 ) ),
'restApiRoutes' => [
'/wc/store/v1' => array_keys( $this->get_routes_from_namespace( 'wc/store/v1' ) ),
],
'defaultAvatar' => get_avatar_url( 0, [ 'force_default' => true ] ),
/*
* translators: If your word count is based on single characters (e.g. East Asian characters),
* enter 'characters_excluding_spaces' or 'characters_including_spaces'. Otherwise, enter 'words'.
* Do not translate into your own language.
*/
'wordCountType' => _x( 'words', 'Word count type. Do not translate!', 'woocommerce' ),
];
if ( is_admin() && ! WC()->is_rest_api_request() ) {
$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' => $published_products,
]
);
}
$this->asset_data_registry->add(
'wcBlocksConfig',
$wc_blocks_config
);
}
}
/**
* Get routes from a REST API namespace.
*
* @param string $namespace Namespace to retrieve.
* @return array
*/
protected function get_routes_from_namespace( $namespace ) {
/**
* Gives opportunity to return routes without invoking the compute intensive REST API.
*
* @since 8.7.0
* @param array $routes Array of routes.
* @param string $namespace Namespace for routes.
* @param string $context Context, can be edit or view.
*/
$routes = apply_filters(
'woocommerce_blocks_pre_get_routes_from_namespace',
array(),
$namespace,
'view'
);
if ( ! empty( $routes ) ) {
return $routes;
}
$rest_server = rest_get_server();
$namespace_index = $rest_server->get_namespace_index(
[
'namespace' => $namespace,
'context' => 'view',
]
);
if ( is_wp_error( $namespace_index ) ) {
return [];
}
$response_data = $namespace_index->get_data();
return $response_data['routes'] ?? [];
}
/**
* Register/enqueue scripts used for this block on the frontend, during render.
*
* @param array $attributes Any attributes that currently are available from the block.
*/
protected function enqueue_scripts( array $attributes = [] ) {
if ( null !== $this->get_block_type_script() ) {
wp_enqueue_script( $this->get_block_type_script( 'handle' ) );
}
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* AbstractDynamicBlock class.
*/
abstract class AbstractDynamicBlock extends AbstractBlock {
/**
* 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 block attributes.
*
* @return array
*/
protected function get_block_type_attributes() {
return array();
}
/**
* Get the schema for the alignment property.
*
* @return array Property definition for align.
*/
protected function get_schema_align() {
return array(
'type' => 'string',
'enum' => array( 'left', 'center', 'right', 'wide', 'full' ),
);
}
/**
* Get the schema for a list of IDs.
*
* @return array Property definition for a list of numeric ids.
*/
protected function get_schema_list_ids() {
return array(
'type' => 'array',
'items' => array(
'type' => 'number',
),
'default' => array(),
);
}
/**
* Get the schema for a boolean value.
*
* @param string $default The default value.
* @return array Property definition.
*/
protected function get_schema_boolean( $default = true ) {
return array(
'type' => 'boolean',
'default' => $default,
);
}
/**
* Get the schema for a numeric value.
*
* @param string $default The default value.
* @return array Property definition.
*/
protected function get_schema_number( $default ) {
return array(
'type' => 'number',
'default' => $default,
);
}
/**
* Get the schema for a string value.
*
* @param string $default The default value.
* @return array Property definition.
*/
protected function get_schema_string( $default = '' ) {
return array(
'type' => 'string',
'default' => $default,
);
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* AbstractInnerBlock class.
*/
abstract class AbstractInnerBlock extends AbstractBlock {
/**
* Is this inner block lazy loaded? this helps us know if we should load its frontend script ot not.
*
* @var boolean
*/
protected $is_lazy_loaded = true;
/**
* Registers the block type with WordPress using the metadata file.
*
* The registration using metadata is now recommended. And it's required for "Inner Blocks" to
* fix the issue of missing translations in the inspector (in the Editor mode)
*/
protected function register_block_type() {
$block_settings = [
'render_callback' => $this->get_block_type_render_callback(),
'editor_style' => $this->get_block_type_editor_style(),
'style' => $this->get_block_type_style(),
];
if ( isset( $this->api_version ) ) {
$block_settings['api_version'] = intval( $this->api_version );
}
$metadata_path = $this->asset_api->get_block_metadata_path( $this->block_name, 'inner-blocks/' );
// Prefer to register with metadata if the path is set in the block's class.
register_block_type_from_metadata(
$metadata_path,
$block_settings
);
}
/**
* For lazy loaded inner blocks, we don't want to enqueue the script but rather leave it for webpack to do that.
*
* @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 ) {
if ( $this->is_lazy_loaded ) {
return null;
}
return parent::get_block_type_script( $key );
}
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
}

View File

@@ -0,0 +1,717 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\BlocksWpQuery;
use Automattic\WooCommerce\StoreApi\SchemaController;
use Automattic\WooCommerce\StoreApi\StoreApi;
use Automattic\WooCommerce\Enums\ProductStockStatus;
/**
* AbstractProductGrid class.
*/
abstract class AbstractProductGrid extends AbstractDynamicBlock {
/**
* Attributes.
*
* @var array
*/
protected $attributes = array();
/**
* InnerBlocks content.
*
* @var string
*/
protected $content = '';
/**
* Query args.
*
* @var array
*/
protected $query_args = array();
/**
* Meta query args.
*
* @var array
*/
protected $meta_query = array();
/**
* Get a set of attributes shared across most of the grid blocks.
*
* @return array List of block attributes with type and defaults.
*/
protected function get_block_type_attributes() {
return array(
'className' => $this->get_schema_string(),
'columns' => $this->get_schema_number( wc_get_theme_support( 'product_blocks::default_columns', 3 ) ),
'rows' => $this->get_schema_number( wc_get_theme_support( 'product_blocks::default_rows', 3 ) ),
'categories' => $this->get_schema_list_ids(),
'catOperator' => array(
'type' => 'string',
'default' => 'any',
),
'contentVisibility' => $this->get_schema_content_visibility(),
'align' => $this->get_schema_align(),
'alignButtons' => $this->get_schema_boolean( false ),
'isPreview' => $this->get_schema_boolean( false ),
'stockStatus' => array(
'type' => 'array',
'default' => array_keys( wc_get_product_stock_status_options() ),
),
);
}
/**
* Include and render the dynamic block.
*
* @param array $attributes Block attributes. Default empty array.
* @param string $content Block content. Default empty string.
* @param WP_Block|null $block Block instance.
* @return string Rendered block type output.
*/
protected function render( $attributes = array(), $content = '', $block = null ) {
$this->attributes = $this->parse_attributes( $attributes );
$this->content = $content;
$this->query_args = $this->parse_query_args();
$products = array_filter( array_map( 'wc_get_product', $this->get_products() ) );
if ( ! $products ) {
return '';
}
/**
* Override product description to prevent infinite loop.
*
* @see https://github.com/woocommerce/woocommerce-blocks/pull/6849
*/
foreach ( $products as $product ) {
$product->set_description( '' );
}
/**
* Product List Render event.
*
* Fires a WP Hook named `experimental__woocommerce_blocks-product-list-render` on render so that the client
* can add event handling when certain products are displayed. This can be used by tracking extensions such
* as Google Analytics to track impressions.
*
* Provides the list of product data (shaped like the Store API responses) and the block name.
*/
$this->asset_api->add_inline_script(
'wp-hooks',
'
window.addEventListener( "DOMContentLoaded", () => {
wp.hooks.doAction(
"experimental__woocommerce_blocks-product-list-render",
{
products: JSON.parse( decodeURIComponent( "' . esc_js(
rawurlencode(
wp_json_encode(
array_map(
[ StoreApi::container()->get( SchemaController::class )->get( 'product' ), 'get_item_response' ],
$products
)
)
)
) . '" ) ),
listName: "' . esc_js( $this->block_name ) . '"
}
);
} );
',
'after'
);
return sprintf(
'<div class="%s"><ul class="wc-block-grid__products">%s</ul></div>',
esc_attr( $this->get_container_classes() ),
implode( '', array_map( array( $this, 'render_product' ), $products ) )
);
}
/**
* Get the schema for the contentVisibility attribute
*
* @return array List of block attributes with type and defaults.
*/
protected function get_schema_content_visibility() {
return array(
'type' => 'object',
'properties' => array(
'image' => $this->get_schema_boolean( true ),
'title' => $this->get_schema_boolean( true ),
'price' => $this->get_schema_boolean( true ),
'rating' => $this->get_schema_boolean( true ),
'button' => $this->get_schema_boolean( true ),
),
);
}
/**
* Get the schema for the orderby attribute.
*
* @return array Property definition of `orderby` attribute.
*/
protected function get_schema_orderby() {
return array(
'type' => 'string',
'enum' => array( 'date', 'popularity', 'price_asc', 'price_desc', 'rating', 'title', 'menu_order' ),
'default' => 'date',
);
}
/**
* Get the block's attributes.
*
* @param array $attributes Block attributes. Default empty array.
* @return array Block attributes merged with defaults.
*/
protected function parse_attributes( $attributes ) {
// These should match what's set in JS `registerBlockType`.
$defaults = array(
'columns' => wc_get_theme_support( 'product_blocks::default_columns', 3 ),
'rows' => wc_get_theme_support( 'product_blocks::default_rows', 3 ),
'alignButtons' => false,
'categories' => array(),
'catOperator' => 'any',
'contentVisibility' => array(
'image' => true,
'title' => true,
'price' => true,
'rating' => true,
'button' => true,
),
'stockStatus' => array_keys( wc_get_product_stock_status_options() ),
);
return wp_parse_args( $attributes, $defaults );
}
/**
* Parse query args.
*
* @return array
*/
protected function parse_query_args() {
// Store the original meta query.
$this->meta_query = WC()->query->get_meta_query();
$query_args = array(
'post_type' => 'product',
'post_status' => 'publish',
'fields' => 'ids',
'ignore_sticky_posts' => true,
'no_found_rows' => false,
'orderby' => '',
'order' => '',
'meta_query' => $this->meta_query, // phpcs:ignore WordPress.DB.SlowDBQuery
'tax_query' => array(), // phpcs:ignore WordPress.DB.SlowDBQuery
'posts_per_page' => $this->get_products_limit(),
);
$this->set_block_query_args( $query_args );
$this->set_ordering_query_args( $query_args );
$this->set_categories_query_args( $query_args );
$this->set_visibility_query_args( $query_args );
$this->set_stock_status_query_args( $query_args );
return $query_args;
}
/**
* Parse query args.
*
* @param array $query_args Query args.
*/
protected function set_ordering_query_args( &$query_args ) {
if ( isset( $this->attributes['orderby'] ) ) {
if ( 'price_desc' === $this->attributes['orderby'] ) {
$query_args['orderby'] = 'price';
$query_args['order'] = 'DESC';
} elseif ( 'price_asc' === $this->attributes['orderby'] ) {
$query_args['orderby'] = 'price';
$query_args['order'] = 'ASC';
} elseif ( 'date' === $this->attributes['orderby'] ) {
$query_args['orderby'] = 'date';
$query_args['order'] = 'DESC';
} else {
$query_args['orderby'] = $this->attributes['orderby'];
}
}
$query_args = array_merge(
$query_args,
WC()->query->get_catalog_ordering_args( $query_args['orderby'], $query_args['order'] )
);
}
/**
* Set args specific to this block
*
* @param array $query_args Query args.
*/
abstract protected function set_block_query_args( &$query_args );
/**
* Set categories query args.
*
* @param array $query_args Query args.
*/
protected function set_categories_query_args( &$query_args ) {
if ( ! empty( $this->attributes['categories'] ) ) {
$categories = array_map( 'absint', $this->attributes['categories'] );
$query_args['tax_query'][] = array(
'taxonomy' => 'product_cat',
'terms' => $categories,
'field' => 'term_id',
'operator' => 'all' === $this->attributes['catOperator'] ? 'AND' : 'IN',
/*
* When cat_operator is AND, the children categories should be excluded,
* as only products belonging to all the children categories would be selected.
*/
'include_children' => 'all' === $this->attributes['catOperator'] ? false : true,
);
}
}
/**
* Set visibility query args.
*
* @param array $query_args Query args.
*/
protected function set_visibility_query_args( &$query_args ) {
$product_visibility_terms = wc_get_product_visibility_term_ids();
$product_visibility_not_in = array( $product_visibility_terms['exclude-from-catalog'] );
if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) {
$product_visibility_not_in[] = $product_visibility_terms[ ProductStockStatus::OUT_OF_STOCK ];
}
$query_args['tax_query'][] = array(
'taxonomy' => 'product_visibility',
'field' => 'term_taxonomy_id',
'terms' => $product_visibility_not_in,
'operator' => 'NOT IN',
);
}
/**
* Set which stock status to use when displaying products.
*
* @param array $query_args Query args.
* @return void
*/
protected function set_stock_status_query_args( &$query_args ) {
$stock_statuses = array_keys( wc_get_product_stock_status_options() );
// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_query
if ( isset( $this->attributes['stockStatus'] ) && $stock_statuses !== $this->attributes['stockStatus'] ) {
// Reset meta_query then update with our stock status.
$query_args['meta_query'] = $this->meta_query;
$query_args['meta_query'][] = array(
'key' => '_stock_status',
'value' => array_merge( [ '' ], $this->attributes['stockStatus'] ),
'compare' => 'IN',
);
} else {
$query_args['meta_query'] = $this->meta_query;
}
// phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_query
}
/**
* Works out the item limit based on rows and columns, or returns default.
*
* @return int
*/
protected function get_products_limit() {
if ( isset( $this->attributes['rows'], $this->attributes['columns'] ) && ! empty( $this->attributes['rows'] ) ) {
$this->attributes['limit'] = intval( $this->attributes['columns'] ) * intval( $this->attributes['rows'] );
}
return intval( $this->attributes['limit'] );
}
/**
* Run the query and return an array of product IDs
*
* @return array List of product IDs
*/
protected function get_products() {
/**
* Filters whether or not the product grid is cacheable.
*
* @param boolean $is_cacheable The list of script dependencies.
* @param array $query_args Query args for the products query passed to BlocksWpQuery.
* @return array True to enable cache, false to disable cache.
*
* @since 2.5.0
*/
$is_cacheable = (bool) apply_filters( 'woocommerce_blocks_product_grid_is_cacheable', true, $this->query_args );
$transient_version = \WC_Cache_Helper::get_transient_version( 'product_query' );
$query = new BlocksWpQuery( $this->query_args );
$results = wp_parse_id_list( $is_cacheable ? $query->get_cached_posts( $transient_version ) : $query->get_posts() );
// Remove ordering query arguments which may have been added by get_catalog_ordering_args.
WC()->query->remove_ordering_args();
// Prime caches to reduce future queries. Note _prime_post_caches is private--we could replace this with our own
// query if it becomes unavailable.
if ( is_callable( '_prime_post_caches' ) ) {
_prime_post_caches( $results );
}
$this->prime_product_variations( $results );
return $results;
}
/**
* Retrieve IDs that are not already present in the cache.
*
* Based on WordPress function: _get_non_cached_ids
*
* @param int[] $product_ids Array of IDs.
* @param string $cache_key The cache bucket to check against.
* @return int[] Array of IDs not present in the cache.
*/
protected function get_non_cached_ids( $product_ids, $cache_key ) {
$non_cached_ids = array();
$cache_values = wp_cache_get_multiple( $product_ids, $cache_key );
foreach ( $cache_values as $id => $value ) {
if ( ! $value ) {
$non_cached_ids[] = (int) $id;
}
}
return $non_cached_ids;
}
/**
* Prime query cache of product variation meta data.
*
* Prepares values in the product_ID_variation_meta_data cache for later use in the ProductSchema::get_variations()
* method. Doing so here reduces the total number of queries needed.
*
* @param int[] $product_ids Product ids to prime variation cache for.
*/
protected function prime_product_variations( $product_ids ) {
$cache_group = 'product_variation_meta_data';
$prime_product_ids = $this->get_non_cached_ids( wp_parse_id_list( $product_ids ), $cache_group );
if ( ! $prime_product_ids ) {
return;
}
global $wpdb;
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
$product_variations = $wpdb->get_results( "SELECT ID as variation_id, post_parent as product_id from {$wpdb->posts} WHERE post_parent IN ( " . implode( ',', $prime_product_ids ) . ' )', ARRAY_A );
$prime_variation_ids = array_column( $product_variations, 'variation_id' );
$variation_ids_by_parent = array_column( $product_variations, 'product_id', 'variation_id' );
if ( empty( $prime_variation_ids ) ) {
return;
}
$all_variation_meta_data = $wpdb->get_results(
$wpdb->prepare(
"SELECT post_id as variation_id, meta_key as attribute_key, meta_value as attribute_value FROM {$wpdb->postmeta} WHERE post_id IN (" . implode( ',', array_map( 'esc_sql', $prime_variation_ids ) ) . ') AND meta_key LIKE %s',
$wpdb->esc_like( 'attribute_' ) . '%'
)
);
// phpcs:enable
// Prepare the data to cache by indexing by the parent product.
$primed_data = array_reduce(
$all_variation_meta_data,
function( $values, $data ) use ( $variation_ids_by_parent ) {
$values[ $variation_ids_by_parent[ $data->variation_id ] ?? 0 ][] = $data;
return $values;
},
array_fill_keys( $prime_product_ids, [] )
);
// Cache everything.
foreach ( $primed_data as $product_id => $variation_meta_data ) {
wp_cache_set(
$product_id,
[
'last_modified' => get_the_modified_date( 'U', $product_id ),
'data' => $variation_meta_data,
],
$cache_group
);
}
}
/**
* Get the list of classes to apply to this block.
*
* @return string space-separated list of classes.
*/
protected function get_container_classes() {
$classes = array(
'wc-block-grid',
"wp-block-{$this->block_name}",
"wp-block-woocommerce-{$this->block_name}",
"wc-block-{$this->block_name}",
"has-{$this->attributes['columns']}-columns",
);
if ( $this->attributes['rows'] > 1 ) {
$classes[] = 'has-multiple-rows';
}
if ( isset( $this->attributes['align'] ) ) {
$classes[] = "align{$this->attributes['align']}";
}
if ( ! empty( $this->attributes['alignButtons'] ) ) {
$classes[] = 'has-aligned-buttons';
}
if ( ! empty( $this->attributes['className'] ) ) {
$classes[] = $this->attributes['className'];
}
return implode( ' ', $classes );
}
/**
* Render a single products.
*
* @param \WC_Product $product Product object.
* @return string Rendered product output.
*/
protected function render_product( $product ) {
$data = (object) array(
'permalink' => esc_url( $product->get_permalink() ),
'image' => $this->get_image_html( $product ),
'title' => $this->get_title_html( $product ),
'rating' => $this->get_rating_html( $product ),
'price' => $this->get_price_html( $product ),
'badge' => $this->get_sale_badge_html( $product ),
'button' => $this->get_button_html( $product ),
);
/**
* Filters the HTML for products in the grid.
*
* @param string $html Product grid item HTML.
* @param array $data Product data passed to the template.
* @param \WC_Product $product Product object.
* @return string Updated product grid item HTML.
*
* @since 2.2.0
*/
return apply_filters(
'woocommerce_blocks_product_grid_item_html',
"<li class=\"wc-block-grid__product\">
<a href=\"{$data->permalink}\" class=\"wc-block-grid__product-link\">
{$data->badge}
{$data->image}
{$data->title}
</a>
{$data->price}
{$data->rating}
{$data->button}
</li>",
$data,
$product
);
}
/**
* Get the product image.
*
* @param \WC_Product $product Product.
* @return string
*/
protected function get_image_html( $product ) {
if ( array_key_exists( 'image', $this->attributes['contentVisibility'] ) && false === $this->attributes['contentVisibility']['image'] ) {
return '';
}
$attr = array(
'alt' => '',
);
if ( $product->get_image_id() ) {
$image_alt = get_post_meta( $product->get_image_id(), '_wp_attachment_image_alt', true );
$attr = array(
'alt' => ( $image_alt ? $image_alt : $product->get_name() ),
);
}
return '<div class="wc-block-grid__product-image">' . $product->get_image( 'woocommerce_thumbnail', $attr ) . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* Get the product title.
*
* @param \WC_Product $product Product.
* @return string
*/
protected function get_title_html( $product ) {
if ( empty( $this->attributes['contentVisibility']['title'] ) ) {
return '';
}
return '<div class="wc-block-grid__product-title">' . wp_kses_post( $product->get_title() ) . '</div>';
}
/**
* Render the rating icons.
*
* @param WC_Product $product Product.
* @return string Rendered product output.
*/
protected function get_rating_html( $product ) {
if ( empty( $this->attributes['contentVisibility']['rating'] ) ) {
return '';
}
$rating_count = $product->get_rating_count();
$average = $product->get_average_rating();
if ( $rating_count > 0 ) {
return sprintf(
'<div class="wc-block-grid__product-rating">%s</div>',
wc_get_rating_html( $average, $rating_count ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
);
}
return '';
}
/**
* Get the price.
*
* @param \WC_Product $product Product.
* @return string Rendered product output.
*/
protected function get_price_html( $product ) {
if ( empty( $this->attributes['contentVisibility']['price'] ) ) {
return '';
}
return sprintf(
'<div class="wc-block-grid__product-price price">%s</div>',
wp_kses_post( $product->get_price_html() )
);
}
/**
* Get the sale badge.
*
* @param \WC_Product $product Product.
* @return string Rendered product output.
*/
protected function get_sale_badge_html( $product ) {
if ( empty( $this->attributes['contentVisibility']['price'] ) ) {
return '';
}
if ( empty( $this->attributes['contentVisibility']['image'] ) ) {
return '';
}
if ( ! $product->is_on_sale() ) {
return;
}
return '<div class="wc-block-grid__product-onsale">
<span aria-hidden="true">' . esc_html__( 'Sale', 'woocommerce' ) . '</span>
<span class="screen-reader-text">' . esc_html__( 'Product on sale', 'woocommerce' ) . '</span>
</div>';
}
/**
* Get the button.
*
* @param \WC_Product $product Product.
* @return string Rendered product output.
*/
protected function get_button_html( $product ) {
if ( empty( $this->attributes['contentVisibility']['button'] ) ) {
return '';
}
return '<div class="wp-block-button wc-block-grid__product-add-to-cart">' . $this->get_add_to_cart( $product ) . '</div>';
}
/**
* Get the "add to cart" button.
*
* @param \WC_Product $product Product.
* @return string Rendered product output.
*/
protected function get_add_to_cart( $product ) {
$attributes = array(
'aria-label' => $product->add_to_cart_description(),
'data-quantity' => '1',
'data-product_id' => $product->get_id(),
'data-product_sku' => $product->get_sku(),
'data-price' => wc_get_price_to_display( $product ),
'rel' => 'nofollow',
'class' => 'wp-block-button__link ' . ( function_exists( 'wc_wp_theme_get_element_class_name' ) ? wc_wp_theme_get_element_class_name( 'button' ) : '' ) . ' add_to_cart_button',
);
if (
$product->supports( 'ajax_add_to_cart' ) &&
$product->is_purchasable() &&
( $product->is_in_stock() || $product->backorders_allowed() )
) {
$attributes['class'] .= ' ajax_add_to_cart';
}
/**
* Filter to manipulate (add/modify/remove) attributes in the HTML code of the generated add to cart button.
*
* @since 8.6.0
*
* @param array $attributes An associative array containing default HTML attributes of the add to cart button.
* @param WC_Product $product The WC_Product instance of the product that will be added to the cart once the button is pressed.
*
* @return array Returns an associative array derived from the default array passed as an argument and added the extra HTML attributes.
*/
$attributes = apply_filters( 'woocommerce_blocks_product_grid_add_to_cart_attributes', $attributes, $product );
return sprintf(
'<a href="%s" %s>%s</a>',
esc_url( $product->add_to_cart_url() ),
wc_implode_html_attributes( $attributes ),
esc_html( $product->add_to_cart_text() )
);
}
/**
* 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 = [] ) {
parent::enqueue_data( $attributes );
$this->asset_data_registry->add( 'minColumns', wc_get_theme_support( 'product_blocks::min_columns', 1 ) );
$this->asset_data_registry->add( 'maxColumns', wc_get_theme_support( 'product_blocks::max_columns', 6 ) );
$this->asset_data_registry->add( 'defaultColumns', wc_get_theme_support( 'product_blocks::default_columns', 3 ) );
$this->asset_data_registry->add( 'minRows', wc_get_theme_support( 'product_blocks::min_rows', 1 ) );
$this->asset_data_registry->add( 'maxRows', wc_get_theme_support( 'product_blocks::max_rows', 6 ) );
$this->asset_data_registry->add( 'defaultRows', wc_get_theme_support( 'product_blocks::default_rows', 3 ) );
}
/**
* Get the frontend style handle for this block type.
*
* @return string[]
*/
protected function get_block_type_style() {
// Currently these blocks rely on the styles from the All Products block.
return [ 'wc-blocks-style', 'wc-blocks-style-all-products' ];
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Automattic\WooCommerce\Blocks\BlockTypes\Accordion;
use Automattic\WooCommerce\Blocks\BlockTypes\AbstractBlock;
use Automattic\WooCommerce\Blocks\BlockTypes\EnableBlockJsonAssetsTrait;
/**
* AccordionGroup class.
*/
class AccordionGroup extends AbstractBlock {
use EnableBlockJsonAssetsTrait;
/**
* Block name.
*
* @var string
*/
protected $block_name = 'accordion-group';
/**
* Include and render the block.
*
* @param array $attributes Block attributes. Default empty array.
* @param string $content Block content. Default empty string.
* @param WP_Block $block Block instance.
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
if ( ! $content ) {
return $content;
}
$p = new \WP_HTML_Tag_Processor( $content );
if ( $p->next_tag( array( 'class_name' => 'wp-block-woocommerce-accordion-group' ) ) ) {
$interactivity_context = array(
'autoclose' => $attributes['autoclose'],
'isOpen' => array(),
);
$p->set_attribute( 'data-wp-interactive', 'woocommerce/accordion' );
$p->set_attribute( 'data-wp-context', wp_json_encode( $interactivity_context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) );
// Only modify content if directives have been set.
$content = $p->get_updated_html();
}
return $content;
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Automattic\WooCommerce\Blocks\BlockTypes\Accordion;
use Automattic\WooCommerce\Blocks\BlockTypes\AbstractBlock;
use Automattic\WooCommerce\Blocks\BlockTypes\EnableBlockJsonAssetsTrait;
/**
* AccordionHeader class.
*/
class AccordionHeader extends AbstractBlock {
use EnableBlockJsonAssetsTrait;
/**
* Block name.
*
* @var string
*/
protected $block_name = 'accordion-header';
}

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Automattic\WooCommerce\Blocks\BlockTypes\Accordion;
use Automattic\WooCommerce\Blocks\BlockTypes\AbstractBlock;
use Automattic\WooCommerce\Blocks\BlockTypes\EnableBlockJsonAssetsTrait;
/**
* AccordionItem class.
*/
class AccordionItem extends AbstractBlock {
use EnableBlockJsonAssetsTrait;
/**
* Block name.
*
* @var string
*/
protected $block_name = 'accordion-item';
/**
* Include and render the block.
*
* @param array $attributes Block attributes. Default empty array.
* @param string $content Block content. Default empty string.
* @param WP_Block $block Block instance.
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
if ( ! $content ) {
return $content;
}
$p = new \WP_HTML_Tag_Processor( $content );
$unique_id = wp_unique_id( 'woocommerce-accordion-item-' );
// Initialize the state of the item on the server using a closure,
// since we need to get derived state based on the current context.
wp_interactivity_state(
'woocommerce/accordion',
array(
'isOpen' => function () {
$context = wp_interactivity_get_context();
return $context['openByDefault'];
},
)
);
if ( $p->next_tag( array( 'class_name' => 'wp-block-woocommerce-accordion-item' ) ) ) {
$interactivity_context = array(
'id' => $unique_id,
'openByDefault' => $attributes['openByDefault'],
);
$p->set_attribute( 'data-wp-context', wp_json_encode( $interactivity_context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) );
$p->set_attribute( 'data-wp-class--is-open', 'state.isOpen' );
$p->set_attribute( 'data-wp-init', 'callbacks.initIsOpen' );
if ( $p->next_tag( array( 'class_name' => 'accordion-item__toggle' ) ) ) {
$p->set_attribute( 'data-wp-on--click', 'actions.toggle' );
$p->set_attribute( 'id', $unique_id );
$p->set_attribute( 'aria-controls', $unique_id . '-panel' );
$p->set_attribute( 'data-wp-bind--aria-expanded', 'state.isOpen' );
if ( $p->next_tag( array( 'class_name' => 'wp-block-woocommerce-accordion-panel' ) ) ) {
$p->set_attribute( 'id', $unique_id . '-panel' );
$p->set_attribute( 'aria-labelledby', $unique_id );
$p->set_attribute( 'role', 'region' );
$p->set_attribute( 'data-wp-bind--inert', '!state.isOpen' );
// Only modify content if all directives have been set.
$content = $p->get_updated_html();
}
}
}
return $content;
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Automattic\WooCommerce\Blocks\BlockTypes\Accordion;
use Automattic\WooCommerce\Blocks\BlockTypes\AbstractBlock;
use Automattic\WooCommerce\Blocks\BlockTypes\EnableBlockJsonAssetsTrait;
/**
* AccordionPanel class.
*/
class AccordionPanel extends AbstractBlock {
use EnableBlockJsonAssetsTrait;
/**
* Block name.
*
* @var string
*/
protected $block_name = 'accordion-panel';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* ActiveFilters class.
*/
class ActiveFilters extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'active-filters';
}

View File

@@ -0,0 +1,280 @@
<?php
declare(strict_types=1);
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
use Automattic\WooCommerce\Blocks\BlockTypes\AddToCartWithOptions\Utils;
use Automattic\WooCommerce\Enums\ProductType;
/**
* AddToCartForm class.
*/
class AddToCartForm extends AbstractBlock {
use EnableBlockJsonAssetsTrait;
/**
* Block name.
*
* @var string
*/
protected $block_name = 'add-to-cart-form';
/**
* Get the block's attributes.
*
* @param array $attributes Block attributes. Default empty array.
* @return array Block attributes merged with defaults.
*/
private function parse_attributes( $attributes ) {
// These should match what's set in JS `registerBlockType`.
$defaults = array(
'quantitySelectorStyle' => 'input',
);
return wp_parse_args( $attributes, $defaults );
}
/**
* Enqueue assets specific to this block.
* We enqueue frontend scripts only if the quantitySelectorStyle is set to 'stepper'.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block $block Block instance.
*/
protected function enqueue_assets( $attributes, $content, $block ) {
$parsed_attributes = $this->parse_attributes( $attributes );
if ( 'stepper' !== $parsed_attributes['quantitySelectorStyle'] ) {
return;
}
parent::enqueue_assets( $attributes, $content, $block );
}
/**
* 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 = [] ) {
parent::enqueue_data( $attributes );
$this->asset_data_registry->add( 'isBlockTheme', wp_is_block_theme() );
}
/**
* Add increment and decrement buttons to the quantity input field.
*
* @param string $product_html Add to Cart form HTML.
* @param string $product_name Product name.
* @return string Add to Cart form HTML with increment and decrement buttons.
*/
private function add_steppers( $product_html, $product_name ) {
// Regex pattern to match the <input> element with id starting with 'quantity_'.
$pattern = '/(<input[^>]*id="quantity_[^"]*"[^>]*\/>)/';
// Replacement string to add button AFTER the matched <input> element.
/* translators: %s refers to the item name in the cart. */
$minus_button = '$1<button aria-label="' . esc_attr( sprintf( __( 'Reduce quantity of %s', 'woocommerce' ), $product_name ) ) . '" type="button" data-wp-on--click="actions.removeQuantity" class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--minus"></button>';
// Replacement string to add button AFTER the matched <input> element.
/* translators: %s refers to the item name in the cart. */
$plus_button = '$1<button aria-label="' . esc_attr( sprintf( __( 'Increase quantity of %s', 'woocommerce' ), $product_name ) ) . '" type="button" data-wp-on--click="actions.addQuantity" class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--plus">+</button>';
$new_html = preg_replace( $pattern, $plus_button, $product_html );
$new_html = preg_replace( $pattern, $minus_button, $new_html );
return $new_html;
}
/**
* Add classes to the Add to Cart form input needed for the stepper style.
*
* @param string $product_html The Add to Cart form HTML.
*
* @return string The Add to Cart form HTML with classes added.
*/
private function add_stepper_classes_to_add_to_cart_form_input( $product_html ) {
$html = new \WP_HTML_Tag_Processor( $product_html );
// Add classes to the form.
while ( $html->next_tag( array( 'class_name' => 'quantity' ) ) ) {
$html->add_class( 'wc-block-components-quantity-selector' );
}
$html = new \WP_HTML_Tag_Processor( $html->get_updated_html() );
while ( $html->next_tag( array( 'class_name' => 'input-text' ) ) ) {
$html->add_class( 'wc-block-components-quantity-selector__input' );
}
return $html->get_updated_html();
}
/**
* Check if a variation product has all attributes set.
* Returns true if the product is not variation, or if all variation attributes have defined values.
*
* @param WC_Product $product The product to check.
*
* @return bool True if all attributes are set, false otherwise.
*/
private function has_all_attributes_set( $product ) {
// If it's not a variation product, return true.
if ( ! $product->is_type( ProductType::VARIATION ) ) {
return true;
}
// Get all variation attributes.
$variation_attributes = $product->get_variation_attributes();
// If there are no variation attributes, return true.
if ( empty( $variation_attributes ) ) {
return true;
}
// Check if any attribute has an empty value (marked as 'any').
if ( in_array( '', array_values( $variation_attributes ), true ) ) {
return false;
}
return true;
}
/**
* Render the block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block $block Block instance.
*
* @return string | void Rendered block output.
*/
protected function render( $attributes, $content, $block ) {
global $product;
$post_id = $block->context['postId'];
if ( ! isset( $post_id ) ) {
return '';
}
$is_descendent_of_single_product_block = is_null( $product ) || $post_id !== $product->get_id();
$previous_product = $product;
$product = wc_get_product( $post_id );
if ( ! $product instanceof \WC_Product ) {
$product = $previous_product;
return '';
}
// Check if all attributes are set for variation product.
if ( $product->is_type( ProductType::VARIATION ) && ! $this->has_all_attributes_set( $product ) ) {
$product = $previous_product;
return '';
}
$is_external_product_with_url = $product instanceof \WC_Product_External && $product->get_product_url();
$managing_stock = $product->managing_stock();
$stock_quantity = $product->get_stock_quantity();
$should_hide_quantity_selector = $product->is_sold_individually() || Utils::is_min_max_quantity_same( $product ) || ( $managing_stock && $stock_quantity <= 1 );
/**
* The stepper buttons don't show when the product is sold individually or stock quantity is less or equal to 1 because the quantity input field is hidden.
* Additionally, if min and max purchase quantity are the same, the buttons should not be rendered at all.
*/
$is_stepper_style = 'stepper' === $attributes['quantitySelectorStyle'] && ! $should_hide_quantity_selector;
if ( $is_descendent_of_single_product_block ) {
add_filter( 'woocommerce_add_to_cart_form_action', array( $this, 'add_to_cart_form_action' ), 10 );
}
ob_start();
/**
* Manage variations in the same way as simple products.
*/
add_action( 'woocommerce_variation_add_to_cart', 'woocommerce_simple_add_to_cart', 10 );
/**
* Trigger the single product add to cart action for each product type.
*
* @since 9.7.0
*/
do_action( 'woocommerce_' . $product->get_type() . '_add_to_cart' );
/**
* Remove the hook to prevent potential conflicts with existing code and extensions.
*/
remove_action( 'woocommerce_variation_add_to_cart', 'woocommerce_simple_add_to_cart', 10 );
$product_html = ob_get_clean();
if ( $is_descendent_of_single_product_block ) {
remove_filter( 'woocommerce_add_to_cart_form_action', array( $this, 'add_to_cart_form_action' ), 10 );
}
if ( ! $product_html ) {
$product = $previous_product;
return '';
}
// If the quantity input is hidden, don't render the stepper buttons and styles.
if ( $is_stepper_style && ! Utils::has_visible_quantity_input( $product_html ) ) {
$is_stepper_style = false;
}
if ( $is_stepper_style ) {
$product_name = $product->get_name();
$product_html = $this->add_steppers( $product_html, $product_name );
$product_html = $this->add_stepper_classes_to_add_to_cart_form_input( $product_html );
}
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes, array(), array( 'extra_classes' ) );
$product_classname = $is_descendent_of_single_product_block ? 'product' : '';
$classes = implode(
' ',
array_filter(
array(
'wp-block-add-to-cart-form wc-block-add-to-cart-form',
esc_attr( $classes_and_styles['classes'] ),
esc_attr( $product_classname ),
$is_stepper_style ? 'wc-block-add-to-cart-form--stepper' : 'wc-block-add-to-cart-form--input',
)
)
);
$wrapper_attributes = get_block_wrapper_attributes(
array(
'class' => $classes,
'style' => esc_attr( $classes_and_styles['styles'] ),
)
);
$form = sprintf(
'<div %1$s %2$s>%3$s</div>',
$wrapper_attributes,
$is_stepper_style ? 'data-wp-interactive="woocommerce/add-to-cart-form"' : '',
$product_html
);
$product = $previous_product;
return $form;
}
/**
* Use current url as the add to cart form action.
*
* @return string The current URL.
*/
public function add_to_cart_form_action() {
global $wp;
return home_url( add_query_arg( $_GET, $wp->request ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
}
}

View File

@@ -0,0 +1,745 @@
<?php
declare(strict_types=1);
namespace Automattic\WooCommerce\Blocks\BlockTypes\AddToCartWithOptions;
use Automattic\WooCommerce\Blocks\BlockTypes\AbstractBlock;
use Automattic\WooCommerce\Blocks\BlockTypes\EnableBlockJsonAssetsTrait;
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
use Automattic\WooCommerce\Enums\ProductType;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
/**
* AddToCartWithOptions class.
*/
class AddToCartWithOptions extends AbstractBlock {
use EnableBlockJsonAssetsTrait;
/**
* Block name.
*
* @var string
*/
protected $block_name = 'add-to-cart-with-options';
/**
* Get the template part path for a product type.
*
* @param string $product_type The product type.
* @return string|bool The template part path if it exists, false otherwise.
*/
protected function get_template_part_path( $product_type ) {
if ( in_array( $product_type, array( ProductType::SIMPLE, ProductType::EXTERNAL, ProductType::VARIABLE, ProductType::GROUPED ), true ) ) {
return Package::get_path() . 'templates/' . BlockTemplateUtils::DIRECTORY_NAMES['TEMPLATE_PARTS'] . '/' . $product_type . '-product-add-to-cart-with-options.html';
}
/**
* Experimental filter for extensions to register a block template part
* for a product type.
*
* @since 9.9.0
* @param string|boolean $template_part_path The template part path if it exists
* @param string $product_type The product type
*/
return apply_filters( '__experimental_woocommerce_' . $product_type . '_add_to_cart_with_options_block_template_part', false, $product_type );
}
/**
* Enqueue assets specific to this block.
* We enqueue frontend scripts only if the product type has a block template
* part (that's WC core product types and extensions that migrated to block
* templates).
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param \WP_Block $block Block instance.
*
* @return void
*/
protected function enqueue_assets( $attributes, $content, $block ) {
$product_id = ( is_object( $block ) && property_exists( $block, 'context' ) && is_array( $block->context ) && array_key_exists( 'postId', $block->context ) ) ? $block->context['postId'] : null;
if ( isset( $product_id ) ) {
$rendered_product = wc_get_product( $product_id );
if ( $rendered_product instanceof \WC_Product ) {
$template_part_path = $this->get_template_part_path( $rendered_product->get_type() );
if ( is_string( $template_part_path ) && '' !== $template_part_path && file_exists( $template_part_path ) ) {
wp_enqueue_script_module( 'woocommerce/add-to-cart-with-options' );
}
}
}
parent::enqueue_assets( $attributes, $content, $block );
}
/**
* 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( 'productTypes', wc_get_product_types() );
$this->asset_data_registry->add( 'addToCartWithOptionsTemplatePartIds', $this->get_template_part_ids() );
}
}
/**
* Get template part IDs for each product type.
*
* @return array Array of product types with their corresponding template part IDs.
*/
protected function get_template_part_ids() {
$product_types = array_keys( wc_get_product_types() );
$current_theme = wp_get_theme()->get_stylesheet();
$template_part_ids = array();
foreach ( $product_types as $product_type ) {
$slug = $product_type . '-product-add-to-cart-with-options';
// Check if theme template exists.
$theme_has_template = BlockTemplateUtils::theme_has_template_part( $slug );
if ( $theme_has_template ) {
$template_part_ids[ $product_type ] = "{$current_theme}//{$slug}";
} else {
$template_part_ids[ $product_type ] = "woocommerce/woocommerce//{$slug}";
}
}
return $template_part_ids;
}
/**
* Modifies the block context for product button blocks when inside the Add to Cart + Options block.
*
* @param array $context The block context.
* @param array $block The parsed block.
* @return array Modified block context.
*/
public function set_is_descendant_of_add_to_cart_with_options_context( $context, $block ) {
if ( 'woocommerce/product-button' === $block['blockName'] ) {
$context['woocommerce/isDescendantOfAddToCartWithOptions'] = true;
}
return $context;
}
/**
* Check if HTML content has form elements.
*
* @param string $html_content The HTML content.
* @return bool True if the HTML content has form elements, false otherwise.
*/
public function has_form_elements( $html_content ) {
$processor = new \WP_HTML_Tag_Processor( $html_content );
$form_elements = array( 'INPUT', 'TEXTAREA', 'SELECT', 'BUTTON', 'FORM' );
while ( $processor->next_tag() ) {
if ( in_array( $processor->get_tag(), $form_elements, true ) ) {
return true;
}
}
return false;
}
/**
* Check if a child product is purchasable.
*
* @param \WC_Product $product The product to check.
* @return bool True if the product is purchasable, false otherwise.
*/
private function is_child_product_purchasable( \WC_Product $product ) {
// Skip variable products.
if ( $product->is_type( ProductType::VARIABLE ) ) {
return false;
}
// Skip grouped products.
if ( $product->is_type( ProductType::GROUPED ) ) {
return false;
}
return $product->is_purchasable() && $product->is_in_stock();
}
/**
* Render the block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param \WP_Block $block Block instance.
*
* @return string|void Rendered block output.
*/
protected function render( $attributes, $content, $block ) {
global $product;
$product_id = ( is_object( $block ) && property_exists( $block, 'context' ) && is_array( $block->context ) && array_key_exists( 'postId', $block->context ) ) ? $block->context['postId'] : null;
if ( ! isset( $product_id ) ) {
return '';
}
$previous_product = $product;
$product = wc_get_product( $product_id );
if ( ! $product instanceof \WC_Product ) {
$product = $previous_product;
return '';
}
// For variations, we display the simple product form.
$product_type = ProductType::VARIATION === $product->get_type() ? ProductType::SIMPLE : $product->get_type();
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes, array(), array( 'extra_classes' ) );
$classes = implode(
' ',
array_filter(
array(
'wp-block-add-to-cart-with-options wc-block-add-to-cart-with-options',
esc_attr( $classes_and_styles['classes'] ),
)
)
);
$template_part_path = $this->get_template_part_path( $product_type );
if ( is_string( $template_part_path ) && '' !== $template_part_path && file_exists( $template_part_path ) ) {
$slug = $product_type . '-product-add-to-cart-with-options';
$template_part_contents = '';
// Determine if we need to load the template part from the DB, the theme or WooCommerce in that order.
$templates_from_db = BlockTemplateUtils::get_block_templates_from_db( array( $slug ), 'wp_template_part' );
if ( is_countable( $templates_from_db ) && count( $templates_from_db ) > 0 ) {
$template_slug_to_load = $templates_from_db[0]->theme;
} else {
$theme_has_template_part = BlockTemplateUtils::theme_has_template_part( $slug );
$template_slug_to_load = $theme_has_template_part ? get_stylesheet() : BlockTemplateUtils::PLUGIN_SLUG;
}
$template_part = get_block_template( $template_slug_to_load . '//' . $slug, 'wp_template_part' );
if ( $template_part && ! empty( $template_part->content ) ) {
$template_part_contents = $template_part->content;
}
if ( '' === $template_part_contents ) {
$template_part_contents = file_get_contents( $template_part_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
}
$default_quantity = $product->get_min_purchase_quantity();
$product_id = $product->get_id();
wp_interactivity_state(
'woocommerce/add-to-cart-with-options',
array(
'isFormValid' => function () use ( $product_id ) {
$product = wc_get_product( $product_id );
if ( $product instanceof \WC_Product && ( $product->is_type( ProductType::GROUPED ) || $product->has_options() ) ) {
return false;
}
return true;
},
)
);
wp_interactivity_config(
'woocommerce/add-to-cart-with-options',
array(
'errorMessages' => array(
'invalidQuantities' => esc_html__(
'Please select a valid quantity to add to the cart.',
'woocommerce'
),
'groupedProductAddToCartMissingItems' => esc_html__(
'Please select some products to add to the cart.',
'woocommerce'
),
'variableProductMissingAttributes' => esc_html__(
'Please select product attributes before adding to cart.',
'woocommerce'
),
'variableProductOutOfStock' => sprintf(
/* translators: %s: product name */
esc_html__(
'You cannot add &quot;%s&quot; to the cart because the product is out of stock.',
'woocommerce'
),
$product->get_name()
),
),
)
);
wp_interactivity_config(
'woocommerce',
array(
'products' => array(
$product->get_id() => array(
'type' => $product->get_type(),
'is_in_stock' => $product->is_in_stock(),
'sold_individually' => $product->is_sold_individually(),
),
),
)
);
$context = array(
'quantity' => array( $product->get_id() => $default_quantity ),
'validationErrors' => array(),
);
if ( $product->is_type( ProductType::VARIABLE ) ) {
$variations_data = array();
$context['selectedAttributes'] = array();
$available_variations = $product->get_available_variations( 'objects' );
foreach ( $available_variations as $variation ) {
// We intentionally set the default quantity to the product's min purchase quantity
// instead of the variation's min purchase quantity. That's because we use the same
// input for all variations, so we want quantities to be in sync.
$context['quantity'][ $variation->get_id() ] = $default_quantity;
$variation_data = array(
'attributes' => $variation->get_variation_attributes(),
'is_in_stock' => $variation->is_in_stock(),
'sold_individually' => $variation->is_sold_individually(),
);
$variations_data[ $variation->get_id() ] = $variation_data;
}
wp_interactivity_config(
'woocommerce',
array(
'products' => array(
$product->get_id() => array(
'variations' => $variations_data,
),
),
)
);
} elseif ( $product->is_type( ProductType::VARIATION ) ) {
$variation_attributes = $product->get_variation_attributes();
$formatted_attributes = array_map(
function ( $key, $value ) {
return [
'attribute' => $key,
'value' => $value,
];
},
array_keys( $variation_attributes ),
$variation_attributes
);
$context['selectedAttributes'] = $formatted_attributes;
} elseif ( $product->is_type( ProductType::GROUPED ) ) {
// Add context for purchasable child products.
$children_product_data = array();
foreach ( $product->get_children() as $child_product_id ) {
$child_product = wc_get_product( $child_product_id );
if ( $child_product && $this->is_child_product_purchasable( $child_product ) ) {
$child_product_quantity_constraints = Utils::get_product_quantity_constraints( $child_product );
$children_product_data[ $child_product_id ] = array(
'min' => $child_product_quantity_constraints['min'],
'max' => $child_product_quantity_constraints['max'],
'step' => $child_product_quantity_constraints['step'],
'type' => $child_product->get_type(),
'is_in_stock' => $child_product->is_in_stock(),
'sold_individually' => $child_product->is_sold_individually(),
);
}
}
$context['groupedProductIds'] = array_keys( $children_product_data );
wp_interactivity_config(
'woocommerce',
array(
'products' => $children_product_data,
)
);
// Add quantity context for purchasable child products.
$context['quantity'] = array_fill_keys(
$context['groupedProductIds'],
0
);
// Set default quantity for each child product.
foreach ( $context['groupedProductIds'] as $child_product_id ) {
$child_product = wc_get_product( $child_product_id );
if ( $child_product ) {
$default_child_quantity = isset( $_POST['quantity'][ $child_product->get_id() ] ) ? wc_stock_amount( wc_clean( wp_unslash( $_POST['quantity'][ $child_product->get_id() ] ) ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing
$context['quantity'][ $child_product_id ] = $default_child_quantity;
// Check for any "sold individually" products and set their default quantity to 0.
if ( $child_product->is_sold_individually() ) {
$context['quantity'][ $child_product_id ] = 0;
}
}
}
}
$hooks_before = '';
$hooks_after = '';
/**
* Filter to disable the compatibility layer for the blockified templates.
*
* This hook allows to disable the compatibility layer for the blockified.
*
* @since 7.6.0
* @param boolean $is_disabled_compatibility_layer Whether the compatibility layer should be disabled.
*/
$is_disabled_compatibility_layer = apply_filters( 'woocommerce_disable_compatibility_layer', false );
if ( ! $is_disabled_compatibility_layer && ! Utils::is_not_purchasable_product( $product ) ) {
ob_start();
/**
* Hook: woocommerce_before_add_to_cart_form.
*
* @since 10.1.0
*/
do_action( 'woocommerce_before_add_to_cart_form' );
if ( ProductType::SIMPLE === $product_type ) {
/**
* Hook: woocommerce_before_add_to_cart_quantity.
*
* @since 10.0.0
*/
do_action( 'woocommerce_before_add_to_cart_quantity' );
/**
* Hook: woocommerce_before_add_to_cart_button.
*
* @since 10.0.0
*/
do_action( 'woocommerce_before_add_to_cart_button' );
} elseif ( ProductType::EXTERNAL === $product_type ) {
/**
* Hook: woocommerce_before_add_to_cart_button.
*
* @since 10.0.0
*/
do_action( 'woocommerce_before_add_to_cart_button' );
} elseif ( ProductType::GROUPED === $product_type ) {
/**
* Hook: woocommerce_before_add_to_cart_button.
*
* @since 10.0.0
*/
do_action( 'woocommerce_before_add_to_cart_button' );
} elseif ( ProductType::VARIABLE === $product_type ) {
/**
* Hook: woocommerce_before_variations_form.
*
* @since 10.0.0
*/
do_action( 'woocommerce_before_variations_form' );
/**
* Hook: woocommerce_after_variations_table.
*
* @since 10.0.0
*/
do_action( 'woocommerce_after_variations_table' );
/**
* Hook: woocommerce_before_single_variation.
*
* @since 10.0.0
*/
do_action( 'woocommerce_before_single_variation' );
// WooCommerce uses `woocommerce_single_variation` to render
// some UI elements like the Add to Cart button for
// variations. We need to remove them to avoid those UI
// elements being duplicate with the blocks.
// We later add these actions back to avoid affecting other
// blocks or templates.
remove_action( 'woocommerce_single_variation', 'woocommerce_single_variation', 10 );
remove_action( 'woocommerce_single_variation', 'woocommerce_single_variation_add_to_cart_button', 20 );
/**
* Hook: woocommerce_single_variation.
*
* @since 10.0.0
*/
do_action( 'woocommerce_single_variation' );
if ( function_exists( 'woocommerce_single_variation' ) ) {
add_action( 'woocommerce_single_variation', 'woocommerce_single_variation', 10 );
}
if ( function_exists( 'woocommerce_single_variation_add_to_cart_button' ) ) {
add_action( 'woocommerce_single_variation', 'woocommerce_single_variation_add_to_cart_button', 20 );
}
/**
* Hook: woocommerce_before_add_to_cart_button.
*
* @since 10.0.0
*/
do_action( 'woocommerce_before_add_to_cart_button' );
/**
* Hook: woocommerce_before_add_to_cart_quantity.
*
* @since 10.0.0
*/
do_action( 'woocommerce_before_add_to_cart_quantity' );
}
$hooks_before = ob_get_clean();
ob_start();
if ( ProductType::SIMPLE === $product_type ) {
/**
* Hook: woocommerce_after_add_to_cart_quantity.
*
* @since 10.0.0
*/
do_action( 'woocommerce_after_add_to_cart_quantity' );
/**
* Hook: woocommerce_after_add_to_cart_button.
*
* @since 10.0.0
*/
do_action( 'woocommerce_after_add_to_cart_button' );
} elseif ( ProductType::EXTERNAL === $product_type ) {
/**
* Hook: woocommerce_after_add_to_cart_button.
*
* @since 10.0.0
*/
do_action( 'woocommerce_after_add_to_cart_button' );
} elseif ( ProductType::GROUPED === $product_type ) {
/**
* Hook: woocommerce_after_add_to_cart_button.
*
* @since 10.0.0
*/
do_action( 'woocommerce_after_add_to_cart_button' );
} elseif ( ProductType::VARIABLE === $product_type ) {
/**
* Hook: woocommerce_after_add_to_cart_quantity.
*
* @since 10.0.0
*/
do_action( 'woocommerce_after_add_to_cart_quantity' );
/**
* Hook: woocommerce_after_add_to_cart_button.
*
* @since 10.0.0
*/
do_action( 'woocommerce_after_add_to_cart_button' );
/**
* Hook: woocommerce_after_single_variation.
*
* @since 10.0.0
*/
do_action( 'woocommerce_after_single_variation' );
/**
* Hook: woocommerce_after_variations_form.
*
* @since 10.0.0
*/
do_action( 'woocommerce_after_variations_form' );
}
/**
* Hook: woocommerce_after_add_to_cart_form.
*
* @since 10.1.0
*/
do_action( 'woocommerce_after_add_to_cart_form' );
$hooks_after = ob_get_clean();
}
// Because we are printing the template part using do_blocks, context from the outside is lost.
// This filter is used to add the isDescendantOfAddToCartWithOptions context back.
add_filter( 'render_block_context', array( $this, 'set_is_descendant_of_add_to_cart_with_options_context' ), 10, 2 );
$template_part_blocks = do_blocks( $template_part_contents );
remove_filter( 'render_block_context', array( $this, 'set_is_descendant_of_add_to_cart_with_options_context' ) );
$wrapper_attributes = array(
'class' => $classes,
'style' => esc_attr( $classes_and_styles['styles'] ),
'data-wp-interactive' => 'woocommerce/add-to-cart-with-options',
'data-wp-class--is-invalid' => '!state.isFormValid',
);
$context_directive = wp_interactivity_data_wp_context( $context );
$cart_redirect_after_add = get_option( 'woocommerce_cart_redirect_after_add' );
$form_attributes = '';
$legacy_mode = 'yes' === $cart_redirect_after_add || $this->has_form_elements( $hooks_before ) || $this->has_form_elements( $hooks_after );
if ( $legacy_mode ) {
$action_url = home_url( add_query_arg( null, null ) );
// If an extension is hooking into the form or we need to redirect to the cart,
// we fall back to a regular HTML form.
$form_attributes = array(
'action' => esc_url(
/**
* Filter the add to cart form action.
*
* @since 10.0.0
* @param string $action_url The add to cart form action URL, defaulting to the current page.
* @return string The add to cart form action URL.
*/
apply_filters( 'woocommerce_add_to_cart_form_action', $action_url )
),
'method' => 'post',
'enctype' => 'multipart/form-data',
'class' => 'cart',
);
} else {
// Otherwise, we use the Interactivity API.
$form_attributes = array(
'data-wp-on--submit' => 'actions.handleSubmit',
);
}
// These hidden inputs are used by extensions or Express Payment methods to gather information of the form state.
$hidden_input = '';
if ( ProductType::SIMPLE === $product_type ) {
$hidden_input = '<input type="hidden" name="add-to-cart" value="' . esc_attr( $product_id ) . '" />';
} elseif ( ProductType::GROUPED === $product_type ) {
$hidden_input = '<input type="hidden" name="add-to-cart" value="' . esc_attr( $product_id ) . '" />';
} elseif ( ProductType::VARIABLE === $product_type ) {
$hidden_input = '<div class="single_variation_wrap">
<input type="hidden" name="add-to-cart" value="' . esc_attr( $product_id ) . '" />
<input type="hidden" name="product_id" value="' . esc_attr( $product_id ) . '" />
<input type="hidden"
name="variation_id"
data-wp-bind--value="woocommerce/product-data::state.variationId"
/>
</div>';
}
$form_html = sprintf(
'<form %1$s %2$s>%3$s%4$s%5$s%6$s</form>',
get_block_wrapper_attributes(
array_merge(
$wrapper_attributes,
$form_attributes,
array(
'class' => implode(
' ',
array_filter(
array(
isset( $wrapper_attributes['class'] ) ? $wrapper_attributes['class'] : '',
isset( $form_attributes['class'] ) ? $form_attributes['class'] : '',
)
)
),
)
)
),
$context_directive,
$hooks_before,
$template_part_blocks,
$hooks_after,
$hidden_input
);
ob_start();
if ( in_array( $product_type, array( ProductType::SIMPLE, ProductType::EXTERNAL, ProductType::VARIABLE, ProductType::GROUPED ), true ) ) {
$add_to_cart_fn = 'woocommerce_' . $product_type . '_add_to_cart';
remove_action( 'woocommerce_' . $product_type . '_add_to_cart', $add_to_cart_fn, 30 );
/**
* Trigger the single product add to cart action that prints the markup.
*
* @since 9.9.0
*/
do_action( 'woocommerce_' . $product_type . '_add_to_cart' );
add_action( 'woocommerce_' . $product_type . '_add_to_cart', $add_to_cart_fn, 30 );
}
$form_html = $form_html . ob_get_clean();
if ( ! $legacy_mode ) {
$form_html = $this->render_interactivity_notices_region( $form_html );
}
} else {
ob_start();
/**
* Trigger the single product add to cart action that prints the markup.
*
* @since 9.7.0
*/
do_action( 'woocommerce_' . $product_type . '_add_to_cart' );
$wrapper_attributes = array(
'class' => $classes,
'style' => esc_attr( $classes_and_styles['styles'] ),
);
$form_html = ob_get_clean();
$form_html = sprintf( '<div %1$s>%2$s</div>', get_block_wrapper_attributes( $wrapper_attributes ), $form_html );
}
$product = $previous_product;
return $form_html;
}
/**
* Render interactivity API powered notices that can be added client-side. This reuses classes
* from the woocommerce/store-notices block to ensure style consistency.
*
* @param string $form_html The form HTML.
* @return string The rendered store notices HTML.
*/
protected function render_interactivity_notices_region( $form_html ) {
$context_directive = wp_interactivity_data_wp_context(
array(
'notices' => array(),
)
);
ob_start();
?>
<div data-wp-interactive="woocommerce/store-notices" class="wc-block-components-notices alignwide" <?php echo $context_directive; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
<template data-wp-each--notice="context.notices" data-wp-each-key="context.notice.id">
<div
class="wc-block-components-notice-banner"
data-wp-class--is-error="state.isError"
data-wp-class--is-success="state.isSuccess"
data-wp-class--is-info="state.isInfo"
data-wp-class--is-dismissible="context.notice.dismissible"
data-wp-bind--role="state.role"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false">
<path data-wp-bind--d="state.iconPath"></path>
</svg>
<div class="wc-block-components-notice-banner__content">
<span data-wp-init="callbacks.renderNoticeContent" aria-live="assertive" aria-atomic="true"></span>
</div>
<button
data-wp-bind--hidden="!context.notice.dismissible"
class="wc-block-components-button wp-element-button wc-block-components-notice-banner__dismiss contained"
aria-label="<?php esc_attr_e( 'Dismiss this notice', 'woocommerce' ); ?>"
data-wp-on--click="actions.removeNotice"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<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" />
</svg>
</button>
</div>
</template>
<?php echo $form_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</div>
<?php
return ob_get_clean();
}
}

View File

@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace Automattic\WooCommerce\Blocks\BlockTypes\AddToCartWithOptions;
use Automattic\WooCommerce\Blocks\BlockTypes\AbstractBlock;
use Automattic\WooCommerce\Blocks\BlockTypes\EnableBlockJsonAssetsTrait;
use Automattic\WooCommerce\Blocks\BlockTypes\AddToCartWithOptions\Utils as AddToCartWithOptionsUtils;
use WP_Block;
/**
* Block type for grouped product selector item in add to cart with options.
* It's responsible to render each child product in a form of a list item.
*/
class GroupedProductItem extends AbstractBlock {
use EnableBlockJsonAssetsTrait;
/**
* Block name.
*
* @var string
*/
protected $block_name = 'add-to-cart-with-options-grouped-product-item';
/**
* Modifies the block context for product price blocks when inside the Grouped Product Selector block.
*
* @param array $context The block context.
* @param array $block The parsed block.
* @return array Modified block context.
*/
public function set_is_descendant_of_grouped_product_selector_context( $context, $block ) {
if (
'woocommerce/product-price' === $block['blockName'] ||
'woocommerce/product-stock-indicator' === $block['blockName']
) {
$context['isDescendantOfGroupedProductSelector'] = true;
}
return $context;
}
/**
* Get product row HTML.
*
* @param string $product_id Product ID.
* @param array $attributes Block attributes.
* @param WP_Block $block The Block.
* @return string Row HTML
*/
private function get_product_row( $product_id, $attributes, $block ): string {
global $post, $product;
$previous_post = $post;
$previous_product = $product;
// Since this template uses the core/post-title block to show the product name
// a temporally replacement of the global post is needed. This is reverted back
// to its initial post value that is stored in the $previous_post variable.
$post = get_post( $product_id ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$product = wc_get_product( $product_id );
add_filter( 'render_block_context', array( $this, 'set_is_descendant_of_grouped_product_selector_context' ), 10, 2 );
// Render the inner blocks of the Post Template block with `dynamic` set to `false` to prevent calling
// `render_callback` and ensure that no wrapper markup is included.
$block_content = AddToCartWithOptionsUtils::render_block_with_context(
$block,
array(
'postType' => 'product',
'postId' => $post->ID,
),
);
remove_filter( 'render_block_context', array( $this, 'set_is_descendant_of_grouped_product_selector_context' ) );
$post = $previous_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$product = $previous_product;
return $block_content;
}
/**
* Render the block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block $block Block instance.
* @return string Rendered block output.
*/
protected function render( $attributes, $content, $block ): string {
global $product;
if ( ! $product instanceof \WC_Product_Grouped ) {
return '';
}
$content = '';
$children = array_filter( array_map( 'wc_get_product', $product->get_children() ), 'wc_products_array_filter_visible_grouped' );
foreach ( $children as $child ) {
$content .= $this->get_product_row( $child->get_id(), $attributes, $block );
}
return $content;
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Automattic\WooCommerce\Blocks\BlockTypes\AddToCartWithOptions;
use Automattic\WooCommerce\Blocks\BlockTypes\AbstractBlock;
use Automattic\WooCommerce\Blocks\BlockTypes\EnableBlockJsonAssetsTrait;
use Automattic\WooCommerce\Blocks\BlockTypes\AddToCartWithOptions\Utils as AddToCartWithOptionsUtils;
use WP_Block;
/**
* Block type for the label of grouped product selector items in Add to Cart + Options.
* It's responsible to render the label for each child product.
*/
class GroupedProductItemLabel extends AbstractBlock {
use EnableBlockJsonAssetsTrait;
/**
* Block name.
*
* @var string
*/
protected $block_name = 'add-to-cart-with-options-grouped-product-item-label';
/**
* Render the block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block $block Block instance.
* @return string Rendered block output.
*/
protected function render( $attributes, $content, $block ): string {
$product = AddToCartWithOptionsUtils::get_product_from_context( $block, $GLOBALS['product'] );
$markup = '';
if ( $product ) {
$wrapper_attributes = get_block_wrapper_attributes();
$title = $product->get_name();
if ( ! $product->is_purchasable() || $product->has_options() || ! $product->is_in_stock() ) {
$markup = sprintf(
'<div %1$s>%2$s</div>',
$wrapper_attributes,
esc_html( $title )
);
} else {
// Checkbox.
$markup = sprintf(
'<label %1$s for="%2$s">%3$s</label>',
$wrapper_attributes,
esc_attr( 'quantity_' . $product->get_id() ),
esc_html( $title )
);
}
}
return $markup;
}
}

View File

@@ -0,0 +1,190 @@
<?php
declare(strict_types=1);
namespace Automattic\WooCommerce\Blocks\BlockTypes\AddToCartWithOptions;
use Automattic\WooCommerce\Blocks\BlockTypes\AbstractBlock;
use Automattic\WooCommerce\Blocks\BlockTypes\EnableBlockJsonAssetsTrait;
use Automattic\WooCommerce\Blocks\BlockTypes\AddToCartWithOptions\Utils as AddToCartWithOptionsUtils;
use WP_Block;
/**
* Block type for the CTA of grouped product selector items in add to cart with options.
* It's responsible to render the CTA for each child product, that might be a button,
* a checkbox, or a link.
*/
class GroupedProductItemSelector extends AbstractBlock {
use EnableBlockJsonAssetsTrait;
/**
* Block name.
*
* @var string
*/
protected $block_name = 'add-to-cart-with-options-grouped-product-item-selector';
/**
* Set the quantity input type to number.
*
* @return string The quantity input type.
*/
public function set_quantity_input_type() {
return 'number';
}
/**
* Gets the quantity selector markup for a product.
*
* @param \WC_Product $product The product object.
* @return string The HTML markup for the quantity selector.
*/
private function get_quantity_selector_markup( $product ) {
ob_start();
$min_value = $product->get_min_purchase_quantity();
$max_value = $product->get_max_purchase_quantity();
if ( $min_value === $max_value && $min_value > 0 ) {
add_filter( 'woocommerce_quantity_input_type', array( $this, 'set_quantity_input_type' ) );
}
woocommerce_quantity_input(
array(
'input_name' => 'quantity[' . $product->get_id() . ']',
'input_id' => 'quantity_' . $product->get_id(),
'input_value' => isset( $_POST['quantity'][ $product->get_id() ] ) ? wc_stock_amount( wc_clean( wp_unslash( $_POST['quantity'][ $product->get_id() ] ) ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification.Missing
'min_value' => 0,
'max_value' => $max_value,
/**
* Filter the placeholder value allowed for the product.
*
* @since 3.10.0
* @param int $max_value Maximum quantity value.
* @param WC_Product $product Product object.
*/
'placeholder' => apply_filters( 'woocommerce_quantity_input_placeholder', 0, $product ),
)
);
if ( $min_value === $max_value && $min_value > 0 ) {
remove_filter( 'woocommerce_quantity_input_type', array( $this, 'set_quantity_input_type' ) );
}
$quantity_html = ob_get_clean();
// Remove the label because we are rendering one as a separate block via GroupedProductItemLabel.
$quantity_html = $this->remove_quantity_label( $quantity_html );
// Modify the quantity input to add stepper buttons.
$product_name = $product->get_name();
$quantity_html = AddToCartWithOptionsUtils::add_quantity_steppers( $quantity_html, $product_name );
$quantity_html = AddToCartWithOptionsUtils::add_quantity_stepper_classes( $quantity_html );
$context = array(
'productId' => $product->get_id(),
'allowZero' => true, // The item is optional in grouped products.
);
// Add interactive data attribute for the stepper functionality.
$quantity_html = AddToCartWithOptionsUtils::make_quantity_input_interactive( $quantity_html, array(), array(), $context );
return $quantity_html;
}
/**
* Removes the label from quantity input HTML.
*
* @param string $quantity_html The quantity input HTML.
* @return string The quantity input HTML without the label.
*/
private function remove_quantity_label( $quantity_html ) {
// Remove the label and aria-label from the quantity input.
$quantity_html = preg_replace( '/<label[^>]*>.*?<\/label>/s', '', $quantity_html );
return preg_replace( '/\s*aria-label="[^"]*"/', '', $quantity_html );
}
/**
* Gets the add to cart button markup for a product.
*
* @param \WC_Product $product_to_render The product object.
* @return string The HTML markup for the add to cart button.
*/
private function get_button_markup( $product_to_render ) {
ob_start();
woocommerce_template_loop_add_to_cart();
$button_html = ob_get_clean();
return $button_html;
}
/**
* Gets the checkbox markup for a product.
*
* @param \WC_Product $product The product object.
* @return string The HTML markup for the checkbox input and label.
*/
private function get_checkbox_markup( $product ) {
if ( $product->is_on_sale() ) {
$label = sprintf(
/* translators: %1$s: Product name. %2$s: Sale price. %3$s: Regular price */
esc_html__( 'Buy one of %1$s on sale for %2$s, original price was %3$s', 'woocommerce' ),
esc_html( $product->get_name() ),
esc_html( wp_strip_all_tags( wc_price( $product->get_price() ) ) ),
esc_html( wp_strip_all_tags( wc_price( $product->get_regular_price() ) ) )
);
} else {
$label = sprintf(
/* translators: %1$s: Product name. %2$s: Product price */
esc_html__( 'Buy one of %1$s for %2$s', 'woocommerce' ),
esc_html( $product->get_name() ),
esc_html( wp_strip_all_tags( wc_price( $product->get_price() ) ) )
);
}
$context_attribute = wp_interactivity_data_wp_context( array( 'productId' => $product->get_id() ) );
return '<input type="checkbox" name="' . esc_attr( 'quantity[' . $product->get_id() . ']' ) . '" value="1" class="wc-grouped-product-add-to-cart-checkbox" id="' . esc_attr( 'quantity_' . $product->get_id() ) . '" data-wp-interactive="woocommerce/add-to-cart-with-options-quantity-selector" data-wp-on--change="actions.handleQuantityCheckboxChange" ' . $context_attribute . ' aria-label="' . esc_attr( $label ) . '"/>';
}
/**
* Render the block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block $block Block instance.
* @return string Rendered block output.
*/
protected function render( $attributes, $content, $block ): string {
global $product;
$previous_product = $product;
$product = AddToCartWithOptionsUtils::get_product_from_context( $block, $previous_product );
$markup = '';
if ( $product ) {
$is_interactive = false;
if ( ! $product->is_purchasable() || $product->has_options() || ! $product->is_in_stock() ) {
$markup = $this->get_button_markup( $product );
} elseif ( $product->is_sold_individually() ) {
$is_interactive = true;
$markup = $this->get_checkbox_markup( $product );
} else {
$is_interactive = true;
$markup = $this->get_quantity_selector_markup( $product );
}
if ( $is_interactive ) {
wp_enqueue_script_module( 'woocommerce/add-to-cart-with-options-quantity-selector' );
}
if ( $markup ) {
$markup = '<div class="wp-block-add-to-cart-with-options-grouped-product-item-selector wc-block-add-to-cart-with-options-grouped-product-item-selector">' . $markup . '</div>';
}
}
$product = $previous_product;
return $markup;
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Automattic\WooCommerce\Blocks\BlockTypes\AddToCartWithOptions;
use Automattic\WooCommerce\Blocks\BlockTypes\AbstractBlock;
use Automattic\WooCommerce\Blocks\BlockTypes\EnableBlockJsonAssetsTrait;
use Automattic\WooCommerce\Enums\ProductType;
/**
* Block type for grouped product selector in add to cart with options.
*/
class GroupedProductSelector extends AbstractBlock {
use EnableBlockJsonAssetsTrait;
/**
* Block name.
*
* @var string
*/
protected $block_name = 'add-to-cart-with-options-grouped-product-selector';
/**
* Render the block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block $block Block instance.
* @return string Rendered block output.
*/
protected function render( $attributes, $content, $block ): string {
global $product;
if ( $product instanceof \WC_Product && $product->is_type( ProductType::GROUPED ) ) {
$p = new \WP_HTML_Tag_Processor( $content );
if ( $p->next_tag( array( 'class_name' => 'wp-block-woocommerce-add-to-cart-with-options-grouped-product-selector' ) ) ) {
$p->set_attribute( 'data-wp-init', 'callbacks.validateQuantities' );
}
return $p->get_updated_html();
}
return '';
}
}

View File

@@ -0,0 +1,180 @@
<?php
declare(strict_types=1);
namespace Automattic\WooCommerce\Blocks\BlockTypes\AddToCartWithOptions;
use Automattic\WooCommerce\Blocks\BlockTypes\AbstractBlock;
use Automattic\WooCommerce\Blocks\BlockTypes\EnableBlockJsonAssetsTrait;
use Automattic\WooCommerce\Blocks\BlockTypes\AddToCartWithOptions\Utils as AddToCartWithOptionsUtils;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
use Automattic\WooCommerce\Enums\ProductType;
/**
* Block type for quantity selector in add to cart with options.
*/
class QuantitySelector extends AbstractBlock {
use EnableBlockJsonAssetsTrait;
/**
* Block name.
*
* @var string
*/
protected $block_name = 'add-to-cart-with-options-quantity-selector';
/**
* Render the block.
*
* The selector is hidden for:
* - Simple products that are out of stock.
* - Not purchasable simple products.
* - External products with URLs
* - Products sold individually
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block $block Block instance.
*
* @return string | void Rendered block output.
*/
protected function render( $attributes, $content, $block ) {
global $product;
$previous_product = $product;
$product = AddToCartWithOptionsUtils::get_product_from_context( $block, $previous_product );
if ( ! $product ) {
$product = $previous_product;
return '';
}
if ( AddToCartWithOptionsUtils::is_not_purchasable_product( $product ) ) {
$product = $previous_product;
return '';
}
$is_external_product_with_url = $product instanceof \WC_Product_External && $product->get_product_url();
$can_only_be_purchased_one_at_a_time = $product->is_sold_individually();
$managing_stock = $product->managing_stock();
$stock_quantity = $product->get_stock_quantity();
$allows_backorders = $product->backorders_allowed();
if ( AddToCartWithOptionsUtils::is_min_max_quantity_same( $product ) ) {
$product = $previous_product;
return '';
}
if ( $is_external_product_with_url || $can_only_be_purchased_one_at_a_time || ( $managing_stock && $stock_quantity <= 1 && ! $allows_backorders ) ) {
$product = $previous_product;
return '';
}
ob_start();
woocommerce_quantity_input(
array(
'min_value' => $product->get_min_purchase_quantity(),
'max_value' => $product->get_max_purchase_quantity(),
'input_value' => isset( $_POST['quantity'] ) ? wc_stock_amount( wp_unslash( $_POST['quantity'] ) ) : $product->get_min_purchase_quantity(), // phpcs:ignore WordPress.Security.NonceVerification.Missing
)
);
$product_html = ob_get_clean();
// If the quantity input is hidden, don't render the stepper buttons and styles.
$has_visible_quantity_input = AddToCartWithOptionsUtils::has_visible_quantity_input( $product_html );
if ( $has_visible_quantity_input ) {
$product_name = $product->get_name();
$product_html = AddToCartWithOptionsUtils::add_quantity_steppers( $product_html, $product_name );
$product_html = AddToCartWithOptionsUtils::add_quantity_stepper_classes( $product_html );
}
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes, array(), array( 'extra_classes' ) );
$classes = implode(
' ',
array_filter(
array(
'wp-block-add-to-cart-with-options-quantity-selector wc-block-add-to-cart-with-options__quantity-selector',
esc_attr( $classes_and_styles['classes'] ),
$has_visible_quantity_input ? '' : 'wc-block-add-to-cart-with-options__quantity-selector--hidden',
)
)
);
$wrapper_attributes = array(
'class' => $classes,
'style' => esc_attr( $classes_and_styles['styles'] ),
);
$input_attributes = array();
$product_quantity_constraints = AddToCartWithOptionsUtils::get_product_quantity_constraints( $product );
wp_interactivity_config(
'woocommerce',
array(
'products' => array(
$product->get_id() => array(
'min' => $product_quantity_constraints['min'],
'max' => $product_quantity_constraints['max'],
'step' => $product_quantity_constraints['step'],
),
),
)
);
if ( $product->is_type( ProductType::VARIABLE ) ) {
wp_enqueue_script_module( 'woocommerce/product-elements' );
$variations_data = $product->get_available_variations( 'objects' );
$formatted_variations_data = array();
foreach ( $variations_data as $variation ) {
$variation_quantity_constraints = AddToCartWithOptionsUtils::get_product_quantity_constraints( $variation );
$variation_data = array();
// Only add variation data if it's different than the defaults.
if ( 1 !== $variation_quantity_constraints['min'] ) {
$variation_data['min'] = $variation_quantity_constraints['min'];
}
if ( null !== $variation_quantity_constraints['max'] ) {
$variation_data['max'] = $variation_quantity_constraints['max'];
}
if ( 1 !== $variation_quantity_constraints['step'] ) {
$variation_data['step'] = $variation_quantity_constraints['step'];
}
if ( $variation->is_sold_individually() ) {
$variation_data['sold_individually'] = true;
}
$formatted_variations_data[ $variation->get_id() ] = $variation_data;
}
wp_interactivity_config(
'woocommerce',
array(
'products' => array(
$product->get_id() => array(
'variations' => $formatted_variations_data,
),
),
)
);
$wrapper_attributes['data-wp-bind--hidden'] = 'woocommerce/add-to-cart-with-options-quantity-selector::!state.allowsQuantityChange';
$input_attributes['data-wp-bind--min'] = 'woocommerce/product-elements::state.productData.min';
$input_attributes['data-wp-bind--max'] = 'woocommerce/product-elements::state.productData.max';
$input_attributes['data-wp-bind--step'] = 'woocommerce/product-elements::state.productData.step';
$input_attributes['data-wp-watch'] = 'woocommerce/add-to-cart-with-options::callbacks.watchQuantityConstraints';
}
$form = AddToCartWithOptionsUtils::make_quantity_input_interactive( $product_html, $wrapper_attributes, $input_attributes );
$product = $previous_product;
return $form;
}
}

View File

@@ -0,0 +1,233 @@
<?php
declare(strict_types=1);
namespace Automattic\WooCommerce\Blocks\BlockTypes\AddToCartWithOptions;
use Automattic\WooCommerce\Enums\ProductType;
use WP_Block;
/**
* Utility methods used for the Add to Cart + Options block.
* {@internal This class and its methods are not intended for public use.}
*/
class Utils {
/**
* Check if the HTML content has a visible quantity input.
*
* @param string $html_content The HTML content.
* @return bool True if the HTML content has a visible input, false otherwise.
*/
public static function has_visible_quantity_input( $html_content ) {
$processor = new \WP_HTML_Tag_Processor( $html_content );
while ( $processor->next_tag() ) {
if (
$processor->get_tag() === 'INPUT' &&
$processor->get_attribute( 'name' ) === 'quantity' &&
$processor->get_attribute( 'type' ) !== 'hidden'
) {
return true;
}
}
return false;
}
/**
* Add increment and decrement buttons to the quantity input field.
*
* @param string $quantity_html Quantity input HTML.
* @param string $product_name Product name.
* @return string Quantity input HTML with increment and decrement buttons.
*/
public static function add_quantity_steppers( $quantity_html, $product_name ) {
// Regex pattern to match the <input> element with id starting with 'quantity_'.
$pattern = '/(<input[^>]*id="quantity_[^"]*"[^>]*\/>)/';
// Replacement string to add button AFTER the matched <input> element.
/* translators: %s refers to the item name in the cart. */
$minus_button = '$1<button aria-label="' . esc_attr( sprintf( __( 'Reduce quantity of %s', 'woocommerce' ), $product_name ) ) . '" type="button" data-wp-on--click="actions.decreaseQuantity" data-wp-bind--disabled="!state.allowsDecrease" class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--minus"></button>';
// Replacement string to add button AFTER the matched <input> element.
/* translators: %s refers to the item name in the cart. */
$plus_button = '$1<button aria-label="' . esc_attr( sprintf( __( 'Increase quantity of %s', 'woocommerce' ), $product_name ) ) . '" type="button" data-wp-on--click="actions.increaseQuantity" data-wp-bind--disabled="!state.allowsIncrease" class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--plus">+</button>';
$new_html = preg_replace( $pattern, $plus_button, $quantity_html );
$new_html = preg_replace( $pattern, $minus_button, $new_html );
return $new_html;
}
/**
* Add classes to the Quantity Selector needed for the stepper style.
*
* @param string $quantity_html The Quantity Selector HTML.
*
* @return string The Quantity Selector HTML with classes added.
*/
public static function add_quantity_stepper_classes( $quantity_html ) {
$processor = new \WP_HTML_Tag_Processor( $quantity_html );
// Add classes to the form.
while ( $processor->next_tag( array( 'class_name' => 'quantity' ) ) ) {
$processor->add_class( 'wc-block-components-quantity-selector' );
}
while ( $processor->next_tag( array( 'class_name' => 'input-text' ) ) ) {
$processor->add_class( 'wc-block-components-quantity-selector__input' );
}
return $processor->get_updated_html();
}
/**
* Make the quantity input interactive by wrapping it with the necessary data attribute and adding a blur event listener.
*
* @param string $quantity_html The quantity HTML.
* @param array $wrapper_attributes Optional wrapper attributes.
* @param array $input_attributes Optional input attributes.
* @param array $context {
* Optional context for quantity input.
* @type int $productId Product ID for context-specific behavior.
* @type bool $allowZero Whether to allow zero quantity.
* }
*
* @return string The quantity HTML with interactive wrapper.
*/
public static function make_quantity_input_interactive( $quantity_html, $wrapper_attributes = array(), $input_attributes = array(), $context = array() ) {
$processor = new \WP_HTML_Tag_Processor( $quantity_html );
global $product;
if (
$processor->next_tag( 'input' ) &&
$processor->get_attribute( 'type' ) === 'number' &&
strpos( $processor->get_attribute( 'name' ), 'quantity' ) !== false
) {
$default_quantity = $product instanceof \WC_Product ? $product->get_min_purchase_quantity() : 1;
$input_quantity = isset( $context['allowZero'] ) && true === $context['allowZero'] ? 0 : $default_quantity;
wp_interactivity_state(
'woocommerce/add-to-cart-with-options-quantity-selector',
array(
'inputQuantity' => $input_quantity,
)
);
$processor->set_attribute( 'data-wp-on--blur', 'actions.handleQuantityBlur' );
$processor->set_attribute( 'data-wp-bind--value', 'state.inputQuantity' );
foreach ( $input_attributes as $attribute => $value ) {
$processor->set_attribute( $attribute, $value );
}
}
$quantity_html = $processor->get_updated_html();
$wrapper_attributes = array_merge(
array(
'data-wp-interactive' => 'woocommerce/add-to-cart-with-options-quantity-selector',
'data-wp-init' => 'callbacks.storeInputElementRef',
),
$wrapper_attributes
);
$context_attribute = wp_interactivity_data_wp_context(
wp_parse_args(
$context,
array(
'productId' => $product instanceof \WC_Product ? $product->get_id() : 0,
)
)
);
return sprintf(
'<div %1$s %2$s>%3$s</div>',
get_block_wrapper_attributes( $wrapper_attributes ),
$context_attribute,
$quantity_html
);
}
/**
* Get product from block context.
*
* @param \WP_Block $block The block instance.
* @param \WC_Product|null $previous_product The previous product (usually from global scope).
* @return \WC_Product|null The product instance or null if not found.
*/
public static function get_product_from_context( $block, $previous_product ) {
$post_id = isset( $block->context['postId'] ) ? $block->context['postId'] : '';
$product = null;
if ( ! empty( $post_id ) ) {
$product = wc_get_product( $post_id );
}
if ( ! $product instanceof \WC_Product && $previous_product instanceof \WC_Product ) {
$product = $previous_product;
}
return $product instanceof \WC_Product ? $product : null;
}
/**
* Check if a product is not purchasable or not in stock.
*
* @param \WC_Product $product The product to check.
* @return bool True if the product is not purchasable or not in stock.
*/
public static function is_not_purchasable_product( $product ) {
if ( $product->is_type( ProductType::SIMPLE ) ) {
return ! $product->is_in_stock() || ! $product->is_purchasable();
} elseif ( $product->is_type( ProductType::VARIABLE ) ) {
return ! $product->is_in_stock() || ! $product->has_purchasable_variations();
}
return false;
}
/**
* Renders a new block with custom context
*
* @param WP_Block $block The block instance.
* @param array $context The context for the new block.
* @return string Rendered block content
*/
public static function render_block_with_context( $block, $context ) {
// Get an instance of the current block.
$block_instance = $block->parsed_block;
// Create new block with custom context.
$new_block = new WP_Block(
$block_instance,
$context
);
// Render with dynamic set to false to prevent calling render_callback.
return $new_block->render( array( 'dynamic' => false ) );
}
/**
* Check if min and max purchase quantity are the same for a product.
*
* @param \WC_Product $product The product to check.
* @return bool True if min and max purchase quantity are the same, false otherwise.
*/
public static function is_min_max_quantity_same( $product ) {
$min_purchase_quantity = $product->get_min_purchase_quantity();
$max_purchase_quantity = $product->get_max_purchase_quantity();
return $min_purchase_quantity === $max_purchase_quantity;
}
/**
* Get the quantity constraints for a product.
*
* @param \WC_Product $product The product to get the quantity constraints for.
* @return array The quantity constraints.
*/
public static function get_product_quantity_constraints( $product ) {
$min = is_numeric( $product->get_min_purchase_quantity() ) ? $product->get_min_purchase_quantity() : 1;
$max_quantity = $product->get_max_purchase_quantity();
$max = is_numeric( $max_quantity ) && -1 !== $max_quantity ? $max_quantity : null;
$step = is_numeric( $product->get_purchase_quantity_step() ) ? $product->get_purchase_quantity_step() : 1;
return array(
'min' => $min,
'max' => $max,
'step' => $step,
);
}
}

View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Automattic\WooCommerce\Blocks\BlockTypes\AddToCartWithOptions;
use Automattic\WooCommerce\Blocks\BlockTypes\AbstractBlock;
use Automattic\WooCommerce\Blocks\BlockTypes\EnableBlockJsonAssetsTrait;
/**
* VariationDescription class.
*/
class VariationDescription extends AbstractBlock {
use EnableBlockJsonAssetsTrait;
/**
* Block name.
*
* @var string
*/
protected $block_name = 'add-to-cart-with-options-variation-description';
/**
* Render the block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block $block Block instance.
*
* @return string Rendered block output.
*/
protected function render( $attributes, $content, $block ) {
global $product;
if ( ! $product instanceof \WC_Product_Variable ) {
return '';
}
$variations = $product->get_available_variations( 'objects' );
$formatted_variations_data = array();
foreach ( $variations as $variation ) {
$variation_description = $variation->get_description();
if ( is_string( $variation_description ) && ! empty( $variation_description ) ) {
$formatted_variations_data[ $variation->get_id() ] = array(
'variation_description' => wp_kses_post( wc_format_content( $variation_description ) ),
);
}
}
wp_interactivity_config(
'woocommerce',
array(
'products' => array(
$product->get_id() => array(
'variations' => $formatted_variations_data,
),
),
)
);
$context_directive = wp_interactivity_data_wp_context(
array(
'productElementKey' => 'variation_description',
)
);
$wrapper_attributes = array(
'data-wp-interactive' => 'woocommerce/product-elements',
'data-wp-bind--hidden' => '!state.productData.variation_description',
'aria-live' => 'polite',
'aria-atomic' => 'true',
);
return '<div ' . $context_directive . ' ' . get_block_wrapper_attributes( $wrapper_attributes ) . ' data-wp-watch="callbacks.updateValue"></div>';
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Automattic\WooCommerce\Blocks\BlockTypes\AddToCartWithOptions;
use Automattic\WooCommerce\Blocks\BlockTypes\AbstractBlock;
use Automattic\WooCommerce\Blocks\BlockTypes\EnableBlockJsonAssetsTrait;
use Automattic\WooCommerce\Enums\ProductType;
/**
* Block type for variation selector in add to cart with options.
*/
class VariationSelector extends AbstractBlock {
use EnableBlockJsonAssetsTrait;
/**
* Block name.
*
* @var string
*/
protected $block_name = 'add-to-cart-with-options-variation-selector';
/**
* Render the block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block $block Block instance.
* @return string Rendered block output.
*/
protected function render( $attributes, $content, $block ): string {
global $product;
if ( $product instanceof \WC_Product && $product->is_type( ProductType::VARIABLE ) && ! Utils::is_not_purchasable_product( $product ) ) {
$p = new \WP_HTML_Tag_Processor( $content );
if ( $p->next_tag( array( 'class_name' => 'wp-block-woocommerce-add-to-cart-with-options-variation-selector' ) ) ) {
$p->set_attribute( 'data-wp-watch', 'callbacks.setSelectedVariationId' );
$p->set_attribute( 'data-wp-watch--validate', 'callbacks.validateVariation' );
}
return $p->get_updated_html();
}
return '';
}
}

View File

@@ -0,0 +1,172 @@
<?php
declare(strict_types=1);
namespace Automattic\WooCommerce\Blocks\BlockTypes\AddToCartWithOptions;
use Automattic\WooCommerce\Blocks\BlockTypes\AbstractBlock;
use Automattic\WooCommerce\Blocks\BlockTypes\EnableBlockJsonAssetsTrait;
use Automattic\WooCommerce\Blocks\BlockTypes\AddToCartWithOptions\Utils as AddToCartWithOptionsUtils;
use WP_Block;
/**
* Block type for variation selector item in add to cart with options.
* It's responsible to render each child attribute in a form of a list item.
*/
class VariationSelectorAttribute extends AbstractBlock {
use EnableBlockJsonAssetsTrait;
/**
* Block name.
*
* @var string
*/
protected $block_name = 'add-to-cart-with-options-variation-selector-attribute';
/**
* Render the block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block $block Block instance.
* @return string Rendered block output.
*/
protected function render( $attributes, $content, $block ): string {
global $product;
$content = '';
$product_attributes = $product->get_variation_attributes();
foreach ( $product_attributes as $product_attribute_name => $product_attribute_terms ) {
$content .= $this->get_product_row( $product_attribute_name, $product_attribute_terms, $block );
}
return $content;
}
/**
* Get product row HTML.
*
* @param string $attribute_name Product Attribute Name.
* @param array $product_attribute_terms Product Attribute Terms.
* @param WP_Block $block The Block.
* @return string Row HTML
*/
private function get_product_row( $attribute_name, $product_attribute_terms, $block ): string {
global $product;
$attribute_terms = $this->get_terms( $attribute_name, $product_attribute_terms );
$product_variations = $product->get_available_variations();
// Filter out terms which are not available in any product variation.
$attribute_terms = array_filter(
$attribute_terms,
function ( $term ) use ( $product_variations, $attribute_name, $attribute_terms ) {
foreach ( $product_variations as $product_variation ) {
if (
$term['value'] === $product_variation['attributes'][ wc_variation_attribute_name( $attribute_name ) ] ||
'' === $product_variation['attributes'][ wc_variation_attribute_name( $attribute_name ) ]
) {
return true;
}
}
}
);
if ( empty( $attribute_terms ) ) {
return '';
}
$block_content = AddToCartWithOptionsUtils::render_block_with_context(
$block,
array(
'woocommerce/attributeId' => 'wc_product_attribute_' . uniqid(),
'woocommerce/attributeName' => $attribute_name,
'woocommerce/attributeTerms' => $attribute_terms,
),
);
// Render the inner blocks of the Variation Selector Item Template block with `dynamic` set to `false`
// to prevent calling `render_callback` and ensure that no wrapper markup is included.
return $block_content;
}
/**
* Get product attributes terms.
*
* @param string $attribute_name Product Attribute Name.
* @param array $attribute_terms Product Attribute Terms.
* @return array[] Array of term data with structure:
* [
* 'label' => (string) Display label for the term.
* 'value' => (string) Internal value/slug for the term.
* 'isSelected' => (bool) Whether this term is the default selection.
* ]
*/
protected function get_terms( $attribute_name, $attribute_terms ) {
global $product;
$is_taxonomy = taxonomy_exists( $attribute_name );
$selected_attribute = $product->get_variation_default_attribute( $attribute_name );
if ( $is_taxonomy ) {
$items = array_map(
function ( $term ) use ( $attribute_name, $product, $selected_attribute ) {
return array(
'value' => $term->slug,
/**
* Filter the variation option name.
*
* @since 9.7.0
*
* @param string $option_label The option label.
* @param WP_Term|string|null $item Term object for taxonomies, option string for custom attributes.
* @param string $attribute_name Name of the attribute.
* @param WC_Product $product Product object.
*/
'label' => apply_filters(
'woocommerce_variation_option_name',
$term->name,
$term,
$attribute_name,
$product
),
'isSelected' => $selected_attribute === $term->slug,
);
},
wc_get_product_terms( $product->get_id(), $attribute_name, array( 'fields' => 'all' ) ),
);
} else {
$items = array_map(
function ( $term ) use ( $attribute_name, $product, $selected_attribute ) {
return array(
'value' => $term,
/**
* Filter the variation option name.
*
* @since 9.7.0
*
* @param string $option_label The option label.
* @param WP_Term|string|null $item Term object for taxonomies, option string for custom attributes.
* @param string $attribute_name Name of the attribute.
* @param WC_Product $product Product object.
*/
'label' => apply_filters(
'woocommerce_variation_option_name',
$term,
null,
$attribute_name,
$product
),
'isSelected' => $selected_attribute === $term,
);
},
$attribute_terms,
);
}
return $items;
}
}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Automattic\WooCommerce\Blocks\BlockTypes\AddToCartWithOptions;
use Automattic\WooCommerce\Blocks\BlockTypes\AbstractBlock;
use Automattic\WooCommerce\Blocks\BlockTypes\EnableBlockJsonAssetsTrait;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
/**
* Block type for variation selector attribute name in add to cart with options.
* It's responsible to render the attribute name.
*/
class VariationSelectorAttributeName extends AbstractBlock {
use EnableBlockJsonAssetsTrait;
/**
* Block name.
*
* @var string
*/
protected $block_name = 'add-to-cart-with-options-variation-selector-attribute-name';
/**
* Render the block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block $block Block instance.
* @return string Rendered block output.
*/
protected function render( $attributes, $content, $block ): string {
if (
! isset(
$block->context['woocommerce/attributeId'],
$block->context['woocommerce/attributeName']
)
) {
return '';
}
$attribute_id = $block->context['woocommerce/attributeId'];
$attribute_name = $block->context['woocommerce/attributeName'];
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes, array(), array( 'extra_classes' ) );
$wrapper_attributes = get_block_wrapper_attributes(
array(
'class' => esc_attr( $classes_and_styles['classes'] ),
'for' => esc_attr( $attribute_id ),
'id' => esc_attr( $attribute_id . '_label' ),
'style' => esc_attr( $classes_and_styles['styles'] ),
)
);
$label_text = esc_html( wc_attribute_label( $attribute_name ) );
return sprintf(
'<label %s>%s</label>',
$wrapper_attributes,
$label_text
);
}
}

View File

@@ -0,0 +1,272 @@
<?php
declare(strict_types=1);
namespace Automattic\WooCommerce\Blocks\BlockTypes\AddToCartWithOptions;
use Automattic\WooCommerce\Blocks\BlockTypes\AbstractBlock;
use Automattic\WooCommerce\Blocks\BlockTypes\EnableBlockJsonAssetsTrait;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
/**
* Block type for variation selector attribute options in add to cart with options.
* It's responsible to render the attribute options.
*/
class VariationSelectorAttributeOptions extends AbstractBlock {
use EnableBlockJsonAssetsTrait;
/**
* Block name.
*
* @var string
*/
protected $block_name = 'add-to-cart-with-options-variation-selector-attribute-options';
/**
* Render the block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block $block Block instance.
* @return string Rendered block output.
*/
protected function render( $attributes, $content, $block ): string {
if (
! isset(
$block->context['woocommerce/attributeName'],
$block->context['woocommerce/attributeId'],
$block->context['woocommerce/attributeTerms']
)
) {
return '';
}
$attribute_slug = wc_variation_attribute_name( $block->context['woocommerce/attributeName'] );
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes, array(), array( 'extra_classes' ) );
$option_style = array_key_exists( 'optionStyle', $attributes ) ? $attributes['optionStyle'] : null;
// During the beta period, `optionStyle` was called `style`, so we check
// `style` for backwards compatibility.
if ( ! $option_style && array_key_exists( 'style', $attributes ) && 'dropdown' === $attributes['style'] ) {
$option_style = 'dropdown';
}
$wrapper_attributes = get_block_wrapper_attributes(
array(
'class' => $classes_and_styles['classes'],
'style' => $classes_and_styles['styles'],
)
);
if ( 'dropdown' === $option_style ) {
$content = $this->render_dropdown( $attributes, $content, $block );
} else {
$content = $this->render_pills( $attributes, $content, $block );
}
return sprintf(
'<div %s>%s</div>',
$wrapper_attributes,
$content
);
}
/**
* Get the normalized version of the attributes.
*
* @param array $attributes The element's attributes.
* @param array $default_attributes The element's default attributes.
* @return string The HTML element's attributes.
*/
public static function get_normalized_attributes( $attributes, $default_attributes = array() ) {
$normalized_attributes = array();
$merged_attributes = array_merge( $default_attributes, $attributes );
foreach ( $merged_attributes as $key => $value ) {
if ( is_null( $value ) ) {
continue;
}
if ( is_array( $value ) || is_object( $value ) ) {
$value = wp_json_encode(
$value,
JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
);
}
$normalized_attributes[] = sprintf( '%s="%s"', esc_attr( $key ), esc_attr( $value ) );
}
return implode( ' ', $normalized_attributes );
}
/**
* Get the default selected attribute.
*
* @param string $attribute_slug The attribute's slug.
* @param array $attribute_terms The attribute's terms.
* @return string|null The default selected attribute.
*/
protected function get_default_selected_attribute( $attribute_slug, $attribute_terms ) {
if ( isset( $_GET[ $attribute_slug ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$raw = wp_unslash( $_GET[ $attribute_slug ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
if ( is_string( $raw ) ) {
$attribute_slug_from_request = sanitize_title( $raw );
foreach ( $attribute_terms as $attribute_term ) {
if ( sanitize_title( $attribute_term['value'] ) === $attribute_slug_from_request ) {
return $attribute_term['value'];
}
}
}
} else {
foreach ( $attribute_terms as $attribute_term ) {
if ( $attribute_term['isSelected'] ) {
return $attribute_term['value'];
}
}
}
return null;
}
/**
* Render the attribute options as pills.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block $block Block instance.
* @return string The pills.
*/
protected function render_pills( $attributes, $content, $block ) {
$attribute_id = $block->context['woocommerce/attributeId'];
$attribute_slug = wc_variation_attribute_name( $block->context['woocommerce/attributeName'] );
$attribute_terms = $block->context['woocommerce/attributeTerms'];
wp_interactivity_state(
'woocommerce/add-to-cart-with-options',
array(
'isOptionSelected' =>
function () {
$context = wp_interactivity_get_context();
return $context['option']['value'] === $context['selectedValue'];
},
)
);
$pills = '';
foreach ( $attribute_terms as $attribute_term ) {
$input = sprintf(
'<input type="radio" %s/>',
$this->get_normalized_attributes(
array(
'class' => 'wc-block-add-to-cart-with-options-variation-selector-attribute-options__pill-input',
'name' => $attribute_slug,
'value' => $attribute_term['value'],
'data-wp-bind--checked' => 'state.isOptionSelected',
'data-wp-bind--disabled' => 'state.isOptionDisabled',
'data-wp-watch' => 'callbacks.watchSelected',
'data-wp-on--click' => 'actions.handlePillClick',
'data-wp-on--keydown' => 'actions.handleKeyDown',
'data-wp-context' => array(
'option' => $attribute_term,
),
),
)
);
$pills .= '<label class="wc-block-add-to-cart-with-options-variation-selector-attribute-options__pill">' . $input . esc_html( $attribute_term['label'] ) . '</label>';
}
return sprintf(
'<div %s>%s</div>',
$this->get_normalized_attributes(
array(
'class' => 'wc-block-add-to-cart-with-options-variation-selector-attribute-options__pills',
'role' => 'radiogroup',
'id' => $attribute_id,
'aria-labelledby' => $attribute_id . '_label',
'data-wp-context' => array(
'name' => $attribute_slug,
'options' => $attribute_terms,
'selectedValue' => $this->get_default_selected_attribute( $attribute_slug, $attribute_terms ),
'focused' => '',
),
'data-wp-init' => 'callbacks.setDefaultSelectedAttribute',
),
),
$pills,
);
}
/**
* Render the attribute options as a dropdown.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block $block Block instance.
* @return string The dropdown.
*/
protected function render_dropdown( $attributes, $content, $block ) {
$attribute_id = $block->context['woocommerce/attributeId'];
$attribute_slug = wc_variation_attribute_name( $block->context['woocommerce/attributeName'] );
$attribute_terms = $block->context['woocommerce/attributeTerms'];
$default_option = array(
'label' => esc_html__( 'Choose an option', 'woocommerce' ),
'value' => '',
'isSelected' => false,
);
$attribute_terms = array_merge(
array( $default_option ),
$attribute_terms
);
$selected_attribute = $this->get_default_selected_attribute( $attribute_slug, $attribute_terms );
$options = '';
foreach ( $attribute_terms as $attribute_term ) {
$option_attributes = array(
'value' => $attribute_term['value'],
'data-wp-bind--disabled' => 'state.isOptionDisabled',
'data-wp-context' => array(
'option' => $attribute_term,
'name' => $attribute_slug,
'options' => $attribute_terms,
),
);
if ( $attribute_term['value'] === $selected_attribute ) {
$option_attributes['selected'] = 'selected';
}
$options .= sprintf(
'<option %s>%s</option>',
$this->get_normalized_attributes(
$option_attributes
),
esc_html( $attribute_term['label'] )
);
}
return sprintf(
'<select %s>%s</select>',
$this->get_normalized_attributes(
array(
'class' => 'wc-block-add-to-cart-with-options-variation-selector-attribute-options__dropdown',
'id' => $attribute_id,
'data-wp-context' => array(
'name' => $attribute_slug,
'options' => $attribute_terms,
'selectedValue' => $selected_attribute,
),
'data-wp-init' => 'callbacks.setDefaultSelectedAttribute',
'data-wp-on--change' => 'actions.handleDropdownChange',
'name' => $attribute_slug,
),
),
$options,
);
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* AllProducts class.
*/
class AllProducts extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'all-products';
/**
* 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 = [] ) {
parent::enqueue_data( $attributes );
// Set this so filter blocks being used as widgets know when to render.
$this->asset_data_registry->add( 'hasFilterableProducts', true );
$this->asset_data_registry->add( 'minColumns', wc_get_theme_support( 'product_blocks::min_columns', 1 ) );
$this->asset_data_registry->add( 'maxColumns', wc_get_theme_support( 'product_blocks::max_columns', 6 ) );
$this->asset_data_registry->add( 'defaultColumns', wc_get_theme_support( 'product_blocks::default_columns', 3 ) );
$this->asset_data_registry->add( 'minRows', wc_get_theme_support( 'product_blocks::min_rows', 1 ) );
$this->asset_data_registry->add( 'maxRows', wc_get_theme_support( 'product_blocks::max_rows', 6 ) );
$this->asset_data_registry->add( 'defaultRows', wc_get_theme_support( 'product_blocks::default_rows', 3 ) );
// Hydrate the All Product block with data from the API. This is for the add to cart buttons which show current quantity in cart, and events.
if ( ! is_admin() && ! WC()->is_rest_api_request() ) {
$this->asset_data_registry->hydrate_api_request( '/wc/store/v1/cart' );
}
}
/**
* It is necessary to register and enqueue assets during the render phase because we want to load assets only if the block has the content.
*/
protected function register_block_type_assets() {
parent::register_block_type_assets();
$this->register_chunk_translations( [ $this->block_name ] );
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* AllReviews class.
*/
class AllReviews extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'all-reviews';
/**
* 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-reviews-block-frontend',
'path' => $this->asset_api->get_block_asset_build_path( 'reviews-frontend' ),
'dependencies' => [],
];
return $key ? $script[ $key ] : $script;
}
/**
* 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 = [] ) {
parent::enqueue_data( $attributes );
$this->asset_data_registry->add( 'reviewRatingsEnabled', wc_review_ratings_enabled() );
$this->asset_data_registry->add( 'showAvatars', '1' === get_option( 'show_avatars' ) );
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* AtomicBlock class.
*
* @internal
*/
class AtomicBlock extends AbstractBlock {
/**
* 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

@@ -0,0 +1,50 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* AttributeFilter class.
*/
class AttributeFilter extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'attribute-filter';
const FILTER_QUERY_VAR_PREFIX = 'filter_';
const QUERY_TYPE_QUERY_VAR_PREFIX = 'query_type_';
/**
* 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 = [] ) {
parent::enqueue_data( $attributes );
$this->asset_data_registry->add( 'attributes', array_values( wc_get_attribute_taxonomies() ) );
// Enqueue any `queryState` that the UI will need to be aware of
// (Ex: the category id if we're on a category page, the tag id if we're on a tag page/etc).
$query_state = [];
if ( is_product_category() ) {
$query_state['category'] = get_queried_object_id();
}
if ( is_product_tag() ) {
$query_state['tag'] = get_queried_object()->term_id;
}
$this->asset_data_registry->add( 'queryState', $query_state );
}
/**
* Get the frontend style handle for this block type.
*
* @return string[]
*/
protected function get_block_type_style() {
return array_merge( parent::get_block_type_style(), [ 'wc-blocks-packages-style' ] );
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
/**
* Breadcrumbs class.
*/
class Breadcrumbs extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'breadcrumbs';
/**
* Render the block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block $block Block instance.
*
* @return string | void Rendered block output.
*/
protected function render( $attributes, $content, $block ) {
ob_start();
woocommerce_breadcrumb();
$breadcrumb = ob_get_clean();
if ( ! $breadcrumb ) {
return;
}
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes, array(), array( 'font_size' ) );
$font_size_classes_and_styles = $this->get_font_size_classes_and_styles( $attributes );
$classes_and_styles['classes'] = $classes_and_styles['classes'] . ' ' . $font_size_classes_and_styles['class'] . ' ';
$classes_and_styles['styles'] = $classes_and_styles['styles'] . ' ' . $font_size_classes_and_styles['style'] . ' ';
return sprintf(
'<div class="woocommerce wp-block-breadcrumbs wc-block-breadcrumbs %1$s" style="%2$s">%3$s</div>',
esc_attr( $classes_and_styles['classes'] ),
esc_attr( $classes_and_styles['styles'] ),
$breadcrumb
);
}
/**
* Get the frontend script handle for this block type.
*
* @param string $key Data to get, or default to everything.
*/
protected function get_block_type_script( $key = null ) {
return null;
}
/**
* Gets font size classes and styles for the breadcrumbs block.
*
* Note: This implementation intentionally avoids using StyleAttributesUtils::get_font_size_class_and_style()
* and get_block_wrapper_attributes() to ensure style attributes take precedence over the class attribute fontSize.
* This is needed because the block.json defines a default fontSize, which is considered an anti-pattern
* since styles should be defined by themes and plugins instead.
*
* @param array $attributes The block attributes.
* @return array The font size classes and styles.
*/
private function get_font_size_classes_and_styles( $attributes ) {
$font_size = $attributes['fontSize'] ?? '';
$custom_font_size = $attributes['style']['typography']['fontSize'] ?? '';
if ( ! $font_size && '' === $custom_font_size ) {
return array(
'class' => null,
'style' => null,
);
}
if ( '' !== $custom_font_size ) {
return array(
'class' => null,
'style' => sprintf( 'font-size: %s;', $custom_font_size ),
);
}
return array(
'class' => sprintf( 'has-font-size has-%s-font-size', $font_size ),
'style' => null,
);
}
}

View File

@@ -0,0 +1,310 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils;
use Automattic\WooCommerce\Internal\FraudProtection\CartEventTracker;
use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionController;
/**
* Cart class.
*
* @internal
*/
class Cart extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'cart';
/**
* Chunks build folder.
*
* @var string
*/
protected $chunks_folder = 'cart-blocks';
/**
* Initialize this block type.
*
* - Hook into WP lifecycle.
* - Register the block with WordPress.
*/
protected function initialize() {
parent::initialize();
add_action( 'wp_loaded', array( $this, 'register_patterns' ) );
}
/**
* Dequeues the scripts added by WC Core to the Cart page.
*
* @return void
*/
public function dequeue_woocommerce_core_scripts() {
wp_dequeue_script( 'wc-cart' );
wp_dequeue_script( 'wc-password-strength-meter' );
wp_dequeue_script( 'selectWoo' );
wp_dequeue_style( 'select2' );
}
/**
* Register block pattern for Empty Cart Message to make it translatable.
*/
public function register_patterns() {
$shop_permalink = wc_get_page_permalink( 'shop' );
register_block_pattern(
'woocommerce/cart-heading',
array(
'title' => '',
'inserter' => false,
'content' => '<!-- wp:heading {"align":"wide", "level":1} --><h1 class="wp-block-heading alignwide">' . esc_html__( 'Cart', 'woocommerce' ) . '</h1><!-- /wp:heading -->',
)
);
register_block_pattern(
'woocommerce/cart-cross-sells-message',
array(
'title' => '',
'inserter' => false,
'content' => '<!-- wp:heading {"fontSize":"large"} --><h2 class="wp-block-heading has-large-font-size">' . esc_html__( 'You may be interested in…', 'woocommerce' ) . '</h2><!-- /wp:heading -->',
)
);
register_block_pattern(
'woocommerce/cart-empty-message',
array(
'title' => '',
'inserter' => false,
'content' => '
<!-- wp:heading {"textAlign":"center","className":"with-empty-cart-icon wc-block-cart__empty-cart__title"} --><h2 class="wp-block-heading has-text-align-center with-empty-cart-icon wc-block-cart__empty-cart__title">' . esc_html__( 'Your cart is currently empty!', 'woocommerce' ) . '</h2><!-- /wp:heading -->
<!-- wp:paragraph {"align":"center"} --><p class="has-text-align-center"><a href="' . esc_attr( esc_url( $shop_permalink ) ) . '">' . esc_html__( 'Browse store', 'woocommerce' ) . '</a></p><!-- /wp:paragraph -->
',
)
);
register_block_pattern(
'woocommerce/cart-new-in-store-message',
array(
'title' => '',
'inserter' => false,
'content' => '<!-- wp:heading {"textAlign":"center"} --><h2 class="wp-block-heading has-text-align-center">' . esc_html__( 'New in store', 'woocommerce' ) . '</h2><!-- /wp:heading -->',
)
);
}
/**
* Get the editor script handle for this block type.
*
* @param string $key Data to get, or default to everything.
* @return array|string;
*/
protected function get_block_type_editor_script( $key = null ) {
$script = [
'handle' => 'wc-' . $this->block_name . '-block',
'path' => $this->asset_api->get_block_asset_build_path( $this->block_name ),
'dependencies' => [ 'wc-blocks' ],
];
return $key ? $script[ $key ] : $script;
}
/**
* 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-' . $this->block_name . '-block-frontend',
'path' => $this->asset_api->get_block_asset_build_path( $this->block_name . '-frontend' ),
'dependencies' => [],
];
return $key ? $script[ $key ] : $script;
}
/**
* Get the frontend style handle for this block type.
*
* @return string[]
*/
protected function get_block_type_style() {
return array_merge( parent::get_block_type_style(), [ 'wc-blocks-packages-style' ] );
}
/**
* Enqueue frontend assets for this block, just in time for rendering.
*
* @param array $attributes Any attributes that currently are available from the block.
* @param string $content The block content.
* @param WP_Block $block The block object.
*/
protected function enqueue_assets( array $attributes, $content, $block ) {
/**
* Fires before cart block scripts are enqueued.
*
* @since 2.6.0
*/
do_action( 'woocommerce_blocks_enqueue_cart_block_scripts_before' );
parent::enqueue_assets( $attributes, $content, $block );
/**
* Fires after cart block scripts are enqueued.
*
* @since 2.6.0
*/
do_action( 'woocommerce_blocks_enqueue_cart_block_scripts_after' );
}
/**
* Append frontend scripts when rendering the Cart 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 ) {
// Dequeue the core scripts when rendering this block.
add_action( 'wp_enqueue_scripts', array( $this, 'dequeue_woocommerce_core_scripts' ), 20 );
// Track cart page loaded for fraud protection.
if ( wc_get_container()->get( FraudProtectionController::class )->feature_is_enabled() ) {
wc_get_container()->get( CartEventTracker::class )
->track_cart_page_loaded();
}
/**
* We need to check if $content has any templates from prior iterations of the block, in order to update to the latest iteration.
* We test the iteration version by searching for new blocks brought in by it.
* The blocks used for testing should be always available in the block (not removable by the user).
*/
$regex_for_filled_cart_block = '/<div[^<]*?data-block-name="woocommerce\/filled-cart-block"[^>]*?>/mi';
// Filled Cart block was added in i2, so we search for it to see if we have a Cart i1 template.
$has_i1_template = ! preg_match( $regex_for_filled_cart_block, $content );
if ( $has_i1_template ) {
/**
* This fallback structure needs to match the defaultTemplate variables defined in the block's edit.tsx files,
* starting from the parent block and going down each inner block, in the order the blocks were registered.
*/
$inner_blocks_html = '$0
<div data-block-name="woocommerce/filled-cart-block" class="wp-block-woocommerce-filled-cart-block">
<div data-block-name="woocommerce/cart-items-block" class="wp-block-woocommerce-cart-items-block">
<div data-block-name="woocommerce/cart-line-items-block" class="wp-block-woocommerce-cart-line-items-block"></div>
</div>
<div data-block-name="woocommerce/cart-totals-block" class="wp-block-woocommerce-cart-totals-block">
<div data-block-name="woocommerce/cart-order-summary-block" class="wp-block-woocommerce-cart-order-summary-block"></div>
<div data-block-name="woocommerce/cart-express-payment-block" class="wp-block-woocommerce-cart-express-payment-block"></div>
<div data-block-name="woocommerce/proceed-to-checkout-block" class="wp-block-woocommerce-proceed-to-checkout-block"></div>
<div data-block-name="woocommerce/cart-accepted-payment-methods-block" class="wp-block-woocommerce-cart-accepted-payment-methods-block"></div>
</div>
</div>
<div data-block-name="woocommerce/empty-cart-block" class="wp-block-woocommerce-empty-cart-block">
';
$content = preg_replace( '/<div class="[a-zA-Z0-9_\- ]*wp-block-woocommerce-cart[a-zA-Z0-9_\- ]*">/mi', $inner_blocks_html, $content );
$content = $content . '</div>';
}
/**
* Cart i3 added inner blocks for Order summary. We need to add them to Cart i2 templates.
* The order needs to match the order in which these blocks were registered.
*/
$order_summary_with_inner_blocks = '$0
<div data-block-name="woocommerce/cart-order-summary-heading-block" class="wp-block-woocommerce-cart-order-summary-heading-block"></div>
<div data-block-name="woocommerce/cart-order-summary-subtotal-block" class="wp-block-woocommerce-cart-order-summary-subtotal-block"></div>
<div data-block-name="woocommerce/cart-order-summary-fee-block" class="wp-block-woocommerce-cart-order-summary-fee-block"></div>
<div data-block-name="woocommerce/cart-order-summary-discount-block" class="wp-block-woocommerce-cart-order-summary-discount-block"></div>
<div data-block-name="woocommerce/cart-order-summary-coupon-form-block" class="wp-block-woocommerce-cart-order-summary-coupon-form-block"></div>
<div data-block-name="woocommerce/cart-order-summary-shipping-form-block" class="wp-block-woocommerce-cart-order-summary-shipping-block"></div>
<div data-block-name="woocommerce/cart-order-summary-taxes-block" class="wp-block-woocommerce-cart-order-summary-taxes-block"></div>
';
// Order summary subtotal block was added in i3, so we search for it to see if we have a Cart i2 template.
$regex_for_order_summary_subtotal = '/<div[^<]*?data-block-name="woocommerce\/cart-order-summary-subtotal-block"[^>]*?>/mi';
$regex_for_order_summary = '/<div[^<]*?data-block-name="woocommerce\/cart-order-summary-block"[^>]*?>/mi';
$has_i2_template = ! preg_match( $regex_for_order_summary_subtotal, $content );
if ( $has_i2_template ) {
$content = preg_replace( $regex_for_order_summary, $order_summary_with_inner_blocks, $content );
}
return $content;
}
/**
* 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 = [] ) {
parent::enqueue_data( $attributes );
$this->asset_data_registry->add( 'countryData', CartCheckoutUtils::get_country_data() );
$this->asset_data_registry->add( 'displayItemizedTaxes', 'itemized' === get_option( 'woocommerce_tax_total_display' ) );
$this->asset_data_registry->add( 'displayCartPricesIncludingTax', 'incl' === get_option( 'woocommerce_tax_display_cart' ) );
$this->asset_data_registry->add( 'taxesEnabled', wc_tax_enabled() );
$this->asset_data_registry->add( 'couponsEnabled', wc_coupons_enabled() );
$this->asset_data_registry->add( 'shippingEnabled', wc_shipping_enabled() );
$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', wp_is_block_theme() );
$this->asset_data_registry->add( 'shippingMethodsExist', CartCheckoutUtils::shipping_methods_exist() > 0 );
// Hydrate the following data depending on admin or frontend context.
if ( ! is_admin() && ! WC()->is_rest_api_request() ) {
$this->asset_data_registry->hydrate_api_request( '/wc/store/v1/cart' );
}
/**
* Fires after cart block data is registered.
*
* @since 2.6.0
*/
do_action( 'woocommerce_blocks_cart_enqueue_data' );
}
/**
* Register script and style assets for the block type before it is registered.
*
* This registers the scripts; it does not enqueue them.
*/
protected function register_block_type_assets() {
parent::register_block_type_assets();
$chunks = $this->get_chunks_paths( $this->chunks_folder );
$vendor_chunks = $this->get_chunks_paths( 'vendors--cart-blocks' );
$shared_chunks = [];
$this->register_chunk_translations( array_merge( $chunks, $vendor_chunks, $shared_chunks ) );
}
/**
* Get list of Cart block & its inner-block types.
*
* @return array;
*/
public static function get_cart_block_types() {
return [
'Cart',
'CartOrderSummaryTaxesBlock',
'CartOrderSummarySubtotalBlock',
'CartOrderSummaryTotalsBlock',
'FilledCartBlock',
'EmptyCartBlock',
'CartTotalsBlock',
'CartItemsBlock',
'CartLineItemsBlock',
'CartOrderSummaryBlock',
'CartExpressPaymentBlock',
'ProceedToCheckoutBlock',
'CartAcceptedPaymentMethodsBlock',
'CartOrderSummaryCouponFormBlock',
'CartOrderSummaryDiscountBlock',
'CartOrderSummaryFeeBlock',
'CartOrderSummaryHeadingBlock',
'CartOrderSummaryShippingBlock',
'CartCrossSellsBlock',
'CartCrossSellsProductsBlock',
];
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CartAcceptedPaymentMethodsBlock class.
*/
class CartAcceptedPaymentMethodsBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'cart-accepted-payment-methods-block';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CartCrossSellsBlock class.
*/
class CartCrossSellsBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'cart-cross-sells-block';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CartCrossSellsProductsBlock class.
*/
class CartCrossSellsProductsBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'cart-cross-sells-products-block';
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CartExpressPaymentBlock class.
*/
class CartExpressPaymentBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @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

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CartItemsBlock class.
*/
class CartItemsBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'cart-items-block';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CartLineItemsBlock class.
*/
class CartLineItemsBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'cart-line-items-block';
}

View File

@@ -0,0 +1,55 @@
<?php // phpcs:ignore Generic.PHP.RequireStrictTypes.MissingDeclaration
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
use Automattic\WooCommerce\Blocks\Utils\MiniCartUtils;
/**
* CartLink class.
*/
class CartLink extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'cart-link';
/**
* Render the block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param \WP_Block $block Block instance.
* @return string | void Rendered block output.
*/
protected function render( $attributes, $content, $block ) {
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
$icon = MiniCartUtils::get_svg_icon( $attributes['cartIcon'] ?? '' );
$text = array_key_exists( 'content', $attributes ) ? esc_html( $attributes['content'] ) : esc_html__( 'Cart', 'woocommerce' );
return sprintf(
'<div %1$s><a class="wc-block-cart-link" href="%2$s">%3$s<span class="wc-block-cart-link__text">%4$s</span></a></div>',
get_block_wrapper_attributes(
array(
'class' => esc_attr( $classes_and_styles['classes'] ),
'style' => $classes_and_styles['styles'],
)
),
esc_url( wc_get_cart_url() ),
$icon,
$text
);
}
/**
* Get the frontend script handle for this block type.
*
* @param string $key Data to get, or default to everything.
*/
protected function get_block_type_script( $key = null ) {
return null;
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CartOrderSummaryBlock class.
*/
class CartOrderSummaryBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'cart-order-summary-block';
/**
* Get the contents of the given inner block.
*
* @param string $block_name Name of the order summary inner block.
* @param string $content The content to search.
* @return array|bool
*/
private function get_inner_block_content( $block_name, $content ) {
if ( preg_match( $this->inner_block_regex( $block_name ), $content, $matches ) ) {
return $matches[0];
}
return false;
}
/**
* Get the regex that will return an inner block.
*
* @param string $block_name Name of the order summary inner block.
* @return string Regex pattern.
*/
private function inner_block_regex( $block_name ) {
return '/<div data-block-name="woocommerce\/cart-order-summary-' . $block_name . '-block"(.+?)>(.*?)<\/div>/si';
}
/**
* Render the Cart Order Summary block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param object $block Block object.
* @return string Rendered block.
*/
protected function render( $attributes, $content, $block ) {
// The order-summary-totals block was introduced as a new parent block for the totals
// (subtotal, discount, fees, shipping and taxes) blocks.
$regex_for_cart_order_summary_totals = '/<div data-block-name="woocommerce\/cart-order-summary-totals-block"(.+?)>/';
$order_summary_totals_content = '<div data-block-name="woocommerce/cart-order-summary-totals-block" class="wp-block-woocommerce-cart-order-summary-totals-block">';
$totals_inner_blocks = array( 'subtotal', 'discount', 'fee', 'shipping', 'taxes' ); // We want to move these blocks inside a parent 'totals' block.
if ( preg_match( $regex_for_cart_order_summary_totals, $content ) ) {
return $content;
}
foreach ( $totals_inner_blocks as $key => $block_name ) {
$inner_block_content = $this->get_inner_block_content( $block_name, $content );
if ( $inner_block_content ) {
$order_summary_totals_content .= "\n" . $inner_block_content;
// The last block is replaced with the totals block.
if ( count( $totals_inner_blocks ) - 1 === $key ) {
$order_summary_totals_content .= '</div>';
$content = preg_replace( $this->inner_block_regex( $block_name ), $order_summary_totals_content, $content );
} else {
// Otherwise, remove the block.
$content = preg_replace( $this->inner_block_regex( $block_name ), '', $content );
}
}
}
return preg_replace( '/\n\n( *?)/i', '', $content );
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CartOrderSummaryCouponFormBlock class.
*/
class CartOrderSummaryCouponFormBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'cart-order-summary-coupon-form-block';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CartOrderSummaryDiscountBlock class.
*/
class CartOrderSummaryDiscountBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'cart-order-summary-discount-block';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CartOrderSummaryFeeBlock class.
*/
class CartOrderSummaryFeeBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'cart-order-summary-fee-block';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CartOrderSummaryHeadingBlock class.
*/
class CartOrderSummaryHeadingBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'cart-order-summary-heading-block';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CartOrderSummaryShippingBlock class.
*/
class CartOrderSummaryShippingBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'cart-order-summary-shipping-block';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CartOrderSummarySubtotalBlock class.
*/
class CartOrderSummarySubtotalBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'cart-order-summary-subtotal-block';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CartOrderSummaryTaxesBlock class.
*/
class CartOrderSummaryTaxesBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'cart-order-summary-taxes-block';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CartOrderSummaryTotalsBlock class.
*/
class CartOrderSummaryTotalsBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'cart-order-summary-totals-block';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CartTotalsBlock class.
*/
class CartTotalsBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'cart-totals-block';
}

View File

@@ -0,0 +1,69 @@
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
/**
* CatalogSorting class.
*/
class CatalogSorting extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'catalog-sorting';
/**
* Render the block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block $block Block instance.
*
* @return string | void Rendered block output.
*/
protected function render( $attributes, $content, $block ) {
ob_start();
woocommerce_catalog_ordering( $attributes );
$catalog_sorting = ob_get_clean();
if ( ! $catalog_sorting ) {
return;
}
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes, array(), array( 'extra_classes' ) );
$wrapper_attributes = get_block_wrapper_attributes(
array(
'class' => implode(
' ',
array_filter(
[
'woocommerce wc-block-catalog-sorting',
esc_attr( $classes_and_styles['classes'] ),
]
)
),
'style' => esc_attr( $styles_and_classes['styles'] ?? '' ),
)
);
return sprintf(
'<div %1$s>%2$s</div>',
$wrapper_attributes,
$catalog_sorting
);
}
/**
* Get the frontend script handle for this block type.
*
* @param string $key Data to get, or default to everything.
*/
protected function get_block_type_script( $key = null ) {
return null;
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CategoryDescription block: renders the current term description using context.
*/
class CategoryDescription extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'category-description';
/**
* Render the block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param \WP_Block $block Block instance.
*
* @return string Rendered block output.
*/
protected function render( $attributes, $content, $block ) {
$term_id = $block->context['termId'] ?? 0;
$term_taxonomy = $block->context['termTaxonomy'] ?? 'product_cat';
$text_align = isset( $attributes['textAlign'] ) ? sanitize_key( $attributes['textAlign'] ) : '';
if ( ! $term_id ) {
return '';
}
$term = get_term( $term_id, $term_taxonomy );
if ( ! $term || is_wp_error( $term ) ) {
return '';
}
$description = $term->description;
if ( empty( trim( $description ) ) ) {
return '';
}
$classes = array();
if ( $text_align ) {
$classes[] = 'has-text-align-' . $text_align;
}
$wrapper_attributes = get_block_wrapper_attributes(
array(
'class' => implode( ' ', $classes ),
)
);
return sprintf(
'<div %1$s>%2$s</div>',
$wrapper_attributes,
wp_kses_post( wc_format_content( $description ) )
);
}
/**
* Register the context used by this block.
*
* @return array
*/
protected function get_block_type_uses_context() {
return [ 'termId', 'termTaxonomy' ];
}
/**
* Disable the style handle for this block.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
}

View File

@@ -0,0 +1,93 @@
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CategoryTitle block: renders the current term title using context.
*/
class CategoryTitle extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'category-title';
/**
* Render the block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param \WP_Block $block Block instance.
*
* @return string Rendered block output.
*/
protected function render( $attributes, $content, $block ) {
$term_id = $block->context['termId'] ?? 0;
$term_taxonomy = $block->context['termTaxonomy'] ?? 'product_cat';
$level = isset( $attributes['level'] ) ? max( 0, min( 6, intval( $attributes['level'] ) ) ) : 2;
$text_align = isset( $attributes['textAlign'] ) ? sanitize_key( $attributes['textAlign'] ) : '';
$is_link = ! empty( $attributes['isLink'] );
$rel = isset( $attributes['rel'] ) ? esc_attr( $attributes['rel'] ) : '';
$target = isset( $attributes['linkTarget'] ) ? esc_attr( $attributes['linkTarget'] ) : '_self';
if ( ! $term_id ) {
return '';
}
$term = get_term( $term_id, $term_taxonomy );
if ( ! $term || is_wp_error( $term ) ) {
return '';
}
$tag_name = 0 === $level ? 'p' : 'h' . $level;
$classes = $text_align ? 'has-text-align-' . $text_align : '';
$wrapper_attributes = get_block_wrapper_attributes( array( 'class' => $classes ) );
$title_html = '';
if ( $is_link ) {
$link = get_term_link( $term );
if ( ! is_wp_error( $link ) ) {
$title_html = sprintf(
'<%1$s %2$s><a href="%3$s" target="%4$s" rel="%5$s">%6$s</a></%1$s>',
esc_attr( $tag_name ),
$wrapper_attributes,
esc_url( $link ),
esc_attr( $target ),
$rel,
esc_html( $term->name )
);
}
}
if ( '' === $title_html ) {
$title_html = sprintf(
'<%1$s %2$s>%3$s</%1$s>',
esc_attr( $tag_name ),
$wrapper_attributes,
esc_html( $term->name )
);
}
return $title_html;
}
/**
* Register the context used by this block.
*
* @return array
*/
protected function get_block_type_uses_context() {
return [ 'termId', 'termTaxonomy' ];
}
/**
* Disable the style handle for this block.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
}

View File

@@ -0,0 +1,641 @@
<?php
declare( strict_types = 1);
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\Block_Scanner;
use Automattic\WooCommerce\StoreApi\Utilities\LocalPickupUtils;
use Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils;
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\StoreApi\Utilities\PaymentUtils;
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFieldsSchema\Validation;
use Automattic\WooCommerce\Internal\AddressProvider\AddressProviderController;
use Automattic\WooCommerce\Internal\FraudProtection\CheckoutEventTracker;
use Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionController;
/**
* Checkout class.
*
* @internal
*/
class Checkout extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'checkout';
/**
* Chunks build folder.
*
* @var string
*/
protected $chunks_folder = 'checkout-blocks';
/**
* Initialize this block type.
*
* - Hook into WP lifecycle.
* - Register the block with WordPress.
*/
protected function initialize() {
parent::initialize();
add_action( 'rest_api_init', array( $this, 'register_settings' ) );
add_action( 'wp_loaded', array( $this, 'register_patterns' ) );
// This prevents the page redirecting when the cart is empty. This is so the editor still loads the page preview.
add_filter(
'woocommerce_checkout_redirect_empty_cart',
function ( $redirect_empty_cart ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
return isset( $_GET['_wp-find-template'] ) ? false : $redirect_empty_cart;
}
);
add_action( 'save_post', array( $this, 'update_local_pickup_title' ), 10, 2 );
}
/**
* Dequeues the scripts added by WC Core to the Checkout page.
*
* @return void
*/
public function dequeue_woocommerce_core_scripts() {
wp_dequeue_script( 'wc-checkout' );
wp_dequeue_script( 'wc-address-autocomplete' );
wp_dequeue_style( 'wc-address-autocomplete' );
wp_dequeue_script( 'wc-password-strength-meter' );
wp_dequeue_script( 'selectWoo' );
wp_dequeue_style( 'select2' );
}
/**
* Exposes settings exposed by the checkout block.
*/
public function register_settings() {
register_setting(
'options',
'woocommerce_checkout_phone_field',
array(
'type' => 'object',
'description' => __( 'Controls the display of the phone field in checkout.', 'woocommerce' ),
'label' => __( 'Phone number', 'woocommerce' ),
'show_in_rest' => array(
'name' => 'woocommerce_checkout_phone_field',
'schema' => array(
'type' => 'string',
'enum' => array( 'optional', 'required', 'hidden' ),
),
),
'default' => CartCheckoutUtils::get_phone_field_visibility(),
)
);
register_setting(
'options',
'woocommerce_checkout_company_field',
array(
'type' => 'object',
'description' => __( 'Controls the display of the company field in checkout.', 'woocommerce' ),
'label' => __( 'Company', 'woocommerce' ),
'show_in_rest' => array(
'name' => 'woocommerce_checkout_company_field',
'schema' => array(
'type' => 'string',
'enum' => array( 'optional', 'required', 'hidden' ),
),
),
'default' => CartCheckoutUtils::get_company_field_visibility(),
)
);
register_setting(
'options',
'woocommerce_checkout_address_2_field',
array(
'type' => 'object',
'description' => __( 'Controls the display of the apartment (address_2) field in checkout.', 'woocommerce' ),
'label' => __( 'Address Line 2', 'woocommerce' ),
'show_in_rest' => array(
'name' => 'woocommerce_checkout_address_2_field',
'schema' => array(
'type' => 'string',
'enum' => array( 'optional', 'required', 'hidden' ),
),
),
'default' => CartCheckoutUtils::get_address_2_field_visibility(),
)
);
}
/**
* Register block pattern for Empty Cart Message to make it translatable.
*/
public function register_patterns() {
register_block_pattern(
'woocommerce/checkout-heading',
array(
'title' => '',
'inserter' => false,
'content' => '<!-- wp:heading {"align":"wide", "level":1} --><h1 class="wp-block-heading alignwide">' . esc_html__( 'Checkout', 'woocommerce' ) . '</h1><!-- /wp:heading -->',
)
);
}
/**
* Get the editor script handle for this block type.
*
* @param string $key Data to get, or default to everything.
* @return array|string;
*/
protected function get_block_type_editor_script( $key = null ) {
$script = [
'handle' => 'wc-' . $this->block_name . '-block',
'path' => $this->asset_api->get_block_asset_build_path( $this->block_name ),
'dependencies' => [ 'wc-blocks' ],
];
return $key ? $script[ $key ] : $script;
}
/**
* 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 ) {
$dependencies = [];
// Load password strength meter script asynchronously if needed.
if ( ! is_user_logged_in() && 'no' === get_option( 'woocommerce_registration_generate_password' ) ) {
$dependencies[] = 'zxcvbn-async';
}
$checkout_fields = Package::container()->get( CheckoutFields::class );
// Load schema parser asynchronously if we need it.
if ( Validation::has_field_schema( $checkout_fields->get_additional_fields() ) ) {
$dependencies[] = 'wc-schema-parser';
}
$script = [
'handle' => 'wc-' . $this->block_name . '-block-frontend',
'path' => $this->asset_api->get_block_asset_build_path( $this->block_name . '-frontend' ),
'dependencies' => $dependencies,
];
return $key ? $script[ $key ] : $script;
}
/**
* Get the frontend style handle for this block type.
*
* @return string[]
*/
protected function get_block_type_style() {
return array_merge( parent::get_block_type_style(), [ 'wc-blocks-packages-style' ] );
}
/**
* Enqueue frontend assets for this block, just in time for rendering.
*
* @param array $attributes Any attributes that currently are available from the block.
* @param string $content The block content.
* @param WP_Block $block The block object.
*/
protected function enqueue_assets( array $attributes, $content, $block ) {
/**
* Fires before checkout block scripts are enqueued.
*
* @since 4.6.0
*/
do_action( 'woocommerce_blocks_enqueue_checkout_block_scripts_before' );
parent::enqueue_assets( $attributes, $content, $block );
/**
* Fires after checkout block scripts are enqueued.
*
* @since 4.6.0
*/
do_action( 'woocommerce_blocks_enqueue_checkout_block_scripts_after' );
}
/**
* Append frontend scripts when rendering 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 ( $this->is_checkout_endpoint() ) {
// Note: Currently the block only takes care of the main checkout form -- if an endpoint is set, refer to the
// legacy shortcode instead and do not render block.
return wp_is_block_theme() ? do_shortcode( '[woocommerce_checkout]' ) : '[woocommerce_checkout]';
}
// Track checkout page loaded for fraud protection.
if ( wc_get_container()->get( FraudProtectionController::class )->feature_is_enabled() ) {
wc_get_container()->get( CheckoutEventTracker::class )
->track_checkout_page_loaded();
}
// Dequeue the core scripts when rendering this block.
add_action( 'wp_enqueue_scripts', array( $this, 'dequeue_woocommerce_core_scripts' ), 20 );
/**
* We need to check if $content has any templates from prior iterations of the block, in order to update to the latest iteration.
* We test the iteration version by searching for new blocks brought in by it.
* The blocks used for testing should be always available in the block (not removable by the user).
* Checkout i1's content was returning an empty div, with no data-block-name attribute
*/
$regex_for_empty_block = '/<div class="[a-zA-Z0-9_\- ]*wp-block-woocommerce-checkout[a-zA-Z0-9_\- ]*"><\/div>/mi';
$has_i1_template = preg_match( $regex_for_empty_block, $content );
if ( $has_i1_template ) {
// This fallback needs to match the default templates defined in our Blocks.
$inner_blocks_html = '
<div data-block-name="woocommerce/checkout-fields-block" class="wp-block-woocommerce-checkout-fields-block">
<div data-block-name="woocommerce/checkout-express-payment-block" class="wp-block-woocommerce-checkout-express-payment-block"></div>
<div data-block-name="woocommerce/checkout-contact-information-block" class="wp-block-woocommerce-checkout-contact-information-block"></div>
<div data-block-name="woocommerce/checkout-shipping-address-block" class="wp-block-woocommerce-checkout-shipping-address-block"></div>
<div data-block-name="woocommerce/checkout-billing-address-block" class="wp-block-woocommerce-checkout-billing-address-block"></div>
<div data-block-name="woocommerce/checkout-shipping-methods-block" class="wp-block-woocommerce-checkout-shipping-methods-block"></div>
<div data-block-name="woocommerce/checkout-payment-block" class="wp-block-woocommerce-checkout-payment-block"></div>
<div data-block-name="woocommerce/checkout-additional-information-block" class="wp-block-woocommerce-checkout-additional-information-block"></div>' .
( isset( $attributes['showOrderNotes'] ) && false === $attributes['showOrderNotes'] ? '' : '<div data-block-name="woocommerce/checkout-order-note-block" class="wp-block-woocommerce-checkout-order-note-block"></div>' ) .
( isset( $attributes['showPolicyLinks'] ) && false === $attributes['showPolicyLinks'] ? '' : '<div data-block-name="woocommerce/checkout-terms-block" class="wp-block-woocommerce-checkout-terms-block"></div>' ) .
'<div data-block-name="woocommerce/checkout-actions-block" class="wp-block-woocommerce-checkout-actions-block"></div>
</div>
<div data-block-name="woocommerce/checkout-totals-block" class="wp-block-woocommerce-checkout-totals-block">
<div data-block-name="woocommerce/checkout-order-summary-block" class="wp-block-woocommerce-checkout-order-summary-block"></div>
</div>
';
$content = str_replace( '</div>', $inner_blocks_html . '</div>', $content );
}
/**
* Checkout i3 added inner blocks for Order summary.
* We need to add them to Checkout i2 templates.
* The order needs to match the order in which these blocks were registered.
*/
$order_summary_with_inner_blocks = '$0
<div data-block-name="woocommerce/checkout-order-summary-cart-items-block" class="wp-block-woocommerce-checkout-order-summary-cart-items-block"></div>
<div data-block-name="woocommerce/checkout-order-summary-subtotal-block" class="wp-block-woocommerce-checkout-order-summary-subtotal-block"></div>
<div data-block-name="woocommerce/checkout-order-summary-fee-block" class="wp-block-woocommerce-checkout-order-summary-fee-block"></div>
<div data-block-name="woocommerce/checkout-order-summary-discount-block" class="wp-block-woocommerce-checkout-order-summary-discount-block"></div>
<div data-block-name="woocommerce/checkout-order-summary-coupon-form-block" class="wp-block-woocommerce-checkout-order-summary-coupon-form-block"></div>
<div data-block-name="woocommerce/checkout-order-summary-shipping-block" class="wp-block-woocommerce-checkout-order-summary-shipping-block"></div>
<div data-block-name="woocommerce/checkout-order-summary-taxes-block" class="wp-block-woocommerce-checkout-order-summary-taxes-block"></div>
';
// Order summary subtotal block was added in i3, so we search for it to see if we have a Checkout i2 template.
$regex_for_order_summary_subtotal = '/<div[^<]*?data-block-name="woocommerce\/checkout-order-summary-subtotal-block"[^>]*?>/mi';
$regex_for_order_summary = '/<div[^<]*?data-block-name="woocommerce\/checkout-order-summary-block"[^>]*?>/mi';
$has_i2_template = ! preg_match( $regex_for_order_summary_subtotal, $content );
if ( $has_i2_template ) {
$content = preg_replace( $regex_for_order_summary, $order_summary_with_inner_blocks, $content );
}
/**
* Add the Local Pickup toggle to checkouts missing this forced template.
*/
$local_pickup_inner_blocks = '<div data-block-name="woocommerce/checkout-shipping-method-block" class="wp-block-woocommerce-checkout-shipping-method-block"></div>' . PHP_EOL . PHP_EOL . '<div data-block-name="woocommerce/checkout-pickup-options-block" class="wp-block-woocommerce-checkout-pickup-options-block"></div>' . PHP_EOL . PHP_EOL . '$0';
$has_local_pickup_regex = '/<div[^<]*?data-block-name="woocommerce\/checkout-shipping-method-block"[^>]*?>/mi';
$has_local_pickup = preg_match( $has_local_pickup_regex, $content );
if ( ! $has_local_pickup ) {
$shipping_address_block_regex = '/<div[^<]*?data-block-name="woocommerce\/checkout-shipping-address-block" class="wp-block-woocommerce-checkout-shipping-address-block"[^>]*?><\/div>/mi';
$content = preg_replace( $shipping_address_block_regex, $local_pickup_inner_blocks, $content );
}
/**
* Add the Additional Information block to checkouts missing it.
*/
$additional_information_inner_blocks = '$0' . PHP_EOL . PHP_EOL . '<div data-block-name="woocommerce/checkout-additional-information-block" class="wp-block-woocommerce-checkout-additional-information-block"></div>' . PHP_EOL . PHP_EOL;
$has_additional_information_regex = '/<div[^<]*?data-block-name="woocommerce\/checkout-additional-information-block"[^>]*?>/mi';
$has_additional_information_block = preg_match( $has_additional_information_regex, $content );
if ( ! $has_additional_information_block ) {
$payment_block_regex = '/<div[^<]*?data-block-name="woocommerce\/checkout-payment-block" class="wp-block-woocommerce-checkout-payment-block"[^>]*?><\/div>/mi';
$content = preg_replace( $payment_block_regex, $additional_information_inner_blocks, $content );
}
return $content;
}
/**
* Check if we're viewing a checkout page endpoint, rather than the main checkout page itself.
*
* @return boolean
*/
protected function is_checkout_endpoint() {
return is_wc_endpoint_url( 'order-pay' ) || is_wc_endpoint_url( 'order-received' );
}
/**
* Update the local pickup title in WooCommerce Settings when the checkout page containing a Checkout block is saved.
*
* @param int $post_id The post ID.
* @param \WP_Post $post The post object.
* @return void
*/
public function update_local_pickup_title( $post_id, $post ) {
// This is not a proper save action, maybe an autosave, so don't continue.
if ( empty( $post->post_status ) || 'inherit' === $post->post_status ) {
return;
}
// Check if we are editing the checkout page and that it contains a Checkout block.
// Cast to string for 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( 'woocommerce/checkout', $post ) || (string) get_option( 'woocommerce_checkout_page_id' ) !== (string) $post_id ) ) {
return;
}
if ( ( ! empty( $post->post_type ) && ! empty( $post->post_name ) && 'page-checkout' !== $post->post_name && 'wp_template' === $post->post_type ) || false === has_block( 'woocommerce/checkout', $post ) ) {
return;
}
$pickup_location_settings = LocalPickupUtils::get_local_pickup_settings( 'edit' );
if ( ! isset( $pickup_location_settings['title'] ) ) {
return;
}
if ( empty( $post->post_content ) ) {
return;
}
$title = $this->find_local_pickup_text_in_checkout_block( $post->post_content );
// Set the title to be an empty string if it isn't a string. This will make it fall back to the default value of "Pickup".
if ( ! is_string( $title ) ) {
$title = '';
}
$pickup_location_settings['title'] = $title;
update_option( 'woocommerce_pickup_location_settings', $pickup_location_settings );
}
/**
* Find the shipping methods block, then get the value of the localPickupText attribute from it.
*
* @param string $post_content The post content to search through.
* @return null|string The local pickup text if found, otherwise null.
*/
private function find_local_pickup_text_in_checkout_block( $post_content ) {
$scanner = Block_Scanner::create( $post_content );
while ( $scanner->next_delimiter() ) {
if ( $scanner->opens_block( 'woocommerce/checkout-shipping-method-block' ) ) {
$attributes = $scanner->allocate_and_return_parsed_attributes();
if ( isset( $attributes['localPickupText'] ) ) {
return $attributes['localPickupText'];
}
}
}
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 = [] ) {
parent::enqueue_data( $attributes );
$country_data = CartCheckoutUtils::get_country_data();
$address_formats = WC()->countries->get_address_formats();
// Move the address format into the 'countryData' setting.
// We need to skip 'default' because that's not a valid country.
foreach ( $address_formats as $country_code => $format ) {
if ( 'default' === $country_code ) {
continue;
}
$country_data[ $country_code ]['format'] = $format;
}
$providers_payload = [];
if ( class_exists( AddressProviderController::class ) && 'no' !== get_option( 'woocommerce_address_autocomplete_enabled', 'no' ) ) {
$controller = wc_get_container()->get( AddressProviderController::class );
$providers = $controller->get_providers();
$providers_payload = array_map(
static function ( $provider ) {
return array(
'id' => (string) $provider->id,
'name' => sanitize_text_field( (string) $provider->name ),
'branding_html' => wp_kses_post( (string) $provider->branding_html ),
);
},
(array) $providers
);
}
$this->asset_data_registry->add( 'addressAutocompleteProviders', $providers_payload );
$this->asset_data_registry->add( 'countryData', $country_data );
$this->asset_data_registry->add( 'defaultAddressFormat', $address_formats['default'] );
$this->asset_data_registry->add(
'checkoutAllowsGuest',
false === filter_var(
wc()->checkout()->is_registration_required(),
FILTER_VALIDATE_BOOLEAN
)
);
$this->asset_data_registry->add(
'checkoutAllowsSignup',
filter_var(
wc()->checkout()->is_registration_enabled(),
FILTER_VALIDATE_BOOLEAN
)
);
$this->asset_data_registry->add( 'checkoutShowLoginReminder', filter_var( get_option( 'woocommerce_enable_checkout_login_reminder' ), FILTER_VALIDATE_BOOLEAN ) );
$this->asset_data_registry->add( 'displayCartPricesIncludingTax', 'incl' === get_option( 'woocommerce_tax_display_cart' ) );
$this->asset_data_registry->add( 'displayItemizedTaxes', 'itemized' === get_option( 'woocommerce_tax_total_display' ) );
$this->asset_data_registry->add( 'forcedBillingAddress', 'billing_only' === get_option( 'woocommerce_ship_to_destination' ) );
$this->asset_data_registry->add( 'generatePassword', filter_var( get_option( 'woocommerce_registration_generate_password' ), FILTER_VALIDATE_BOOLEAN ) );
$this->asset_data_registry->add( 'taxesEnabled', wc_tax_enabled() );
$this->asset_data_registry->add( 'couponsEnabled', wc_coupons_enabled() );
$this->asset_data_registry->add( 'shippingEnabled', wc_shipping_enabled() );
$this->asset_data_registry->add( 'hasDarkEditorStyleSupport', current_theme_supports( 'dark-editor-style' ) );
$this->asset_data_registry->register_page_id( isset( $attributes['cartPageId'] ) ? $attributes['cartPageId'] : 0 );
$this->asset_data_registry->add( 'isBlockTheme', wp_is_block_theme() );
$this->asset_data_registry->add( 'isCheckoutBlock', true );
$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( 'localPickupCost', $pickup_location_settings['cost'] );
$this->asset_data_registry->add( 'collectableMethodIds', $local_pickup_method_ids );
$this->asset_data_registry->add( 'shippingMethodsExist', CartCheckoutUtils::shipping_methods_exist() );
$is_block_editor = $this->is_block_editor();
if ( $is_block_editor ) {
$this->asset_data_registry->add(
'localPickupLocations',
array_filter(
array_map(
function ( $location ) {
if ( ! wc_string_to_bool( $location['enabled'] ) ) {
return null;
}
$location['formatted_address'] = wc()->countries->get_formatted_address( $location['address'], ', ' );
return $location;
},
LocalPickupUtils::get_local_pickup_method_locations()
)
),
);
}
if ( $is_block_editor && ! $this->asset_data_registry->exists( 'globalShippingMethods' ) ) {
$shipping_methods = WC()->shipping()->get_shipping_methods();
$formatted_shipping_methods = array_reduce(
$shipping_methods,
function ( $acc, $method ) use ( $local_pickup_method_ids ) {
if ( in_array( $method->id, $local_pickup_method_ids, true ) ) {
return $acc;
}
if ( $method->supports( 'settings' ) ) {
$acc[] = [
'id' => $method->id,
'title' => $method->method_title,
'description' => $method->method_description,
];
}
return $acc;
},
[]
);
$this->asset_data_registry->add( 'globalShippingMethods', $formatted_shipping_methods );
}
if ( $is_block_editor && ! $this->asset_data_registry->exists( 'activeShippingZones' ) && class_exists( '\WC_Shipping_Zones' ) ) {
$this->asset_data_registry->add( 'activeShippingZones', CartCheckoutUtils::get_shipping_zones() );
}
if ( $is_block_editor && ! $this->asset_data_registry->exists( 'globalPaymentMethods' ) ) {
// These are used to show options in the sidebar. We want to get the full list of enabled payment methods,
// not just the ones that are available for the current cart (which may not exist yet).
$payment_methods = PaymentUtils::get_enabled_payment_gateways();
$formatted_payment_methods = array_reduce(
$payment_methods,
function ( $acc, $method ) {
$acc[] = [
'id' => $method->id,
'title' => $method->get_method_title() !== '' ? $method->get_method_title() : $method->get_title(),
'description' => $method->get_method_description() !== '' ? $method->get_method_description() : $method->get_description(),
];
return $acc;
},
[]
);
$this->asset_data_registry->add( 'globalPaymentMethods', $formatted_payment_methods );
}
if ( $is_block_editor && ! $this->asset_data_registry->exists( 'incompatibleExtensions' ) ) {
if ( ! class_exists( '\Automattic\WooCommerce\Utilities\FeaturesUtil' ) || ! function_exists( 'get_plugins' ) ) {
return;
}
$declared_extensions = \Automattic\WooCommerce\Utilities\FeaturesUtil::get_compatible_plugins_for_feature( 'cart_checkout_blocks' );
$all_plugins = \get_plugins(); // Note that `get_compatible_plugins_for_feature` calls `get_plugins` internally, so this is already in cache.
$incompatible_extensions = array_reduce(
$declared_extensions['incompatible'],
function ( $acc, $item ) use ( $all_plugins ) {
$plugin = $all_plugins[ $item ] ?? null;
$plugin_id = $plugin['TextDomain'] ?? dirname( $item, 2 );
$plugin_name = $plugin['Name'] ?? $plugin_id;
$acc[] = [
'id' => $plugin_id,
'title' => $plugin_name,
];
return $acc;
},
[]
);
$this->asset_data_registry->add( 'incompatibleExtensions', $incompatible_extensions );
}
if ( ! is_admin() && ! WC()->is_rest_api_request() ) {
$this->asset_data_registry->hydrate_api_request( '/wc/store/v1/cart' );
$this->asset_data_registry->hydrate_data_from_api_request( 'checkoutData', '/wc/store/v1/checkout' );
$this->hydrate_customer_payment_methods();
}
/**
* Fires after checkout block data is registered.
*
* @since 2.6.0
*/
do_action( 'woocommerce_blocks_checkout_enqueue_data' );
}
/**
* Get saved customer payment methods for use in checkout.
*/
protected function hydrate_customer_payment_methods() {
$payment_methods = PaymentUtils::get_saved_payment_methods();
if ( ! $payment_methods || $this->asset_data_registry->exists( 'customerPaymentMethods' ) ) {
return;
}
$this->asset_data_registry->add(
'customerPaymentMethods',
is_array( $payment_methods ) ? $payment_methods['enabled'] : null
);
}
/**
* Register script and style assets for the block type before it is registered.
*
* This registers the scripts; it does not enqueue them.
*/
protected function register_block_type_assets() {
parent::register_block_type_assets();
$chunks = $this->get_chunks_paths( $this->chunks_folder );
$vendor_chunks = $this->get_chunks_paths( 'vendors--checkout-blocks' );
$shared_chunks = [ 'cart-blocks/cart-express-payment--checkout-blocks/express-payment-frontend' ];
$this->register_chunk_translations( array_merge( $chunks, $vendor_chunks, $shared_chunks ) );
}
/**
* Get list of Checkout block & its inner-block types.
*
* @return array;
*/
public static function get_checkout_block_types() {
return [
'Checkout',
'CheckoutActionsBlock',
'CheckoutAdditionalInformationBlock',
'CheckoutBillingAddressBlock',
'CheckoutContactInformationBlock',
'CheckoutExpressPaymentBlock',
'CheckoutFieldsBlock',
'CheckoutOrderNoteBlock',
'CheckoutOrderSummaryBlock',
'CheckoutOrderSummaryCartItemsBlock',
'CheckoutOrderSummaryCouponFormBlock',
'CheckoutOrderSummaryDiscountBlock',
'CheckoutOrderSummaryFeeBlock',
'CheckoutOrderSummaryShippingBlock',
'CheckoutOrderSummarySubtotalBlock',
'CheckoutOrderSummaryTaxesBlock',
'CheckoutOrderSummaryTotalsBlock',
'CheckoutPaymentBlock',
'CheckoutShippingAddressBlock',
'CheckoutShippingMethodsBlock',
'CheckoutShippingMethodBlock',
'CheckoutPickupOptionsBlock',
'CheckoutTermsBlock',
'CheckoutTotalsBlock',
];
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CheckoutActionsBlock class.
*/
class CheckoutActionsBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'checkout-actions-block';
/**
* Initialize this block type.
*
* - Hook into WP lifecycle.
* - Register the block with WordPress.
*/
protected function initialize() {
parent::initialize();
add_action( 'wp_loaded', array( $this, 'register_style_variations' ) );
}
/**
* Register style variations for the block.
*/
public function register_style_variations() {
register_block_style(
$this->get_full_block_name(),
array(
'name' => 'without-price',
'label' => __( 'Hide Price', 'woocommerce' ),
'is_default' => true,
)
);
register_block_style(
$this->get_full_block_name(),
array(
'name' => 'with-price',
'label' => __( 'Show Price', 'woocommerce' ),
'is_default' => false,
)
);
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CheckoutAdditionalInformationBlock class.
*/
class CheckoutAdditionalInformationBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'checkout-additional-information-block';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CheckoutBillingAddressBlock class.
*/
class CheckoutBillingAddressBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'checkout-billing-address-block';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CheckoutContactInformationBlock class.
*/
class CheckoutContactInformationBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'checkout-contact-information-block';
}

View File

@@ -0,0 +1,140 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils;
use Exception;
/**
* CheckoutExpressPaymentBlock class.
*/
class CheckoutExpressPaymentBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @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.
$attrs = CartCheckoutUtils::find_express_checkout_attributes( $post->post_content, $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

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CheckoutFieldsBlock class.
*/
class CheckoutFieldsBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'checkout-fields-block';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CheckoutOrderNoteBlock class.
*/
class CheckoutOrderNoteBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'checkout-order-note-block';
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CheckoutOrderSummaryBlock class.
*/
class CheckoutOrderSummaryBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'checkout-order-summary-block';
/**
* Get the contents of the given inner block.
*
* @param string $block_name Name of the order summary inner block.
* @param string $content The content to search.
* @return array|bool
*/
private function get_inner_block_content( $block_name, $content ) {
if ( preg_match( $this->inner_block_regex( $block_name ), $content, $matches ) ) {
return $matches[0];
}
return false;
}
/**
* Get the regex that will return an inner block.
*
* @param string $block_name Name of the order summary inner block.
* @return string Regex pattern.
*/
private function inner_block_regex( $block_name ) {
return '/<div data-block-name="woocommerce\/checkout-order-summary-' . $block_name . '-block"(.+?)>(.*?)<\/div>/si';
}
/**
* Render the Checkout Order Summary block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param object $block Block object.
* @return string Rendered block.
*/
protected function render( $attributes, $content, $block ) {
// The order-summary-totals block was introduced as a new parent block for the totals
// (subtotal, discount, fees, shipping and taxes) blocks.
$regex_for_checkout_order_summary_totals = '/<div data-block-name="woocommerce\/checkout-order-summary-totals-block"(.+?)>/';
$order_summary_totals_content = '<div data-block-name="woocommerce/checkout-order-summary-totals-block" class="wp-block-woocommerce-checkout-order-summary-totals-block">';
// We want to move these blocks inside a parent 'totals' block.
$totals_inner_blocks = array( 'subtotal', 'discount', 'fee', 'shipping', 'taxes' );
if ( preg_match( $regex_for_checkout_order_summary_totals, $content ) ) {
return $content;
}
foreach ( $totals_inner_blocks as $key => $block_name ) {
$inner_block_content = $this->get_inner_block_content( $block_name, $content );
if ( $inner_block_content ) {
$order_summary_totals_content .= "\n" . $inner_block_content;
// The last block is replaced with the totals block.
if ( count( $totals_inner_blocks ) - 1 === $key ) {
$order_summary_totals_content .= '</div>';
$content = preg_replace( $this->inner_block_regex( $block_name ), $order_summary_totals_content, $content );
} else {
// Otherwise, remove the block.
$content = preg_replace( $this->inner_block_regex( $block_name ), '', $content );
}
}
}
// Remove empty lines.
return preg_replace( '/\n\n( *?)/i', '', $content );
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CheckoutOrderSummaryCartItemsBlock class.
*/
class CheckoutOrderSummaryCartItemsBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'checkout-order-summary-cart-items-block';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CheckoutOrderSummaryCouponFormBlock class.
*/
class CheckoutOrderSummaryCouponFormBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'checkout-order-summary-coupon-form-block';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CheckoutOrderSummaryDiscountBlock class.
*/
class CheckoutOrderSummaryDiscountBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'checkout-order-summary-discount-block';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CheckoutOrderSummaryFeeBlock class.
*/
class CheckoutOrderSummaryFeeBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'checkout-order-summary-fee-block';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CheckoutOrderSummaryShippingBlock class.
*/
class CheckoutOrderSummaryShippingBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'checkout-order-summary-shipping-block';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CheckoutOrderSummarySubtotalBlock class.
*/
class CheckoutOrderSummarySubtotalBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'checkout-order-summary-subtotal-block';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CheckoutOrderSummaryTaxesBlock class.
*/
class CheckoutOrderSummaryTaxesBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'checkout-order-summary-taxes-block';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CheckoutOrderSummaryTotalsBlock class.
*/
class CheckoutOrderSummaryTotalsBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'checkout-order-summary-totals-block';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CheckoutPaymentBlock class.
*/
class CheckoutPaymentBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'checkout-payment-block';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CheckoutPickupOptionsBlock class.
*/
class CheckoutPickupOptionsBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'checkout-pickup-options-block';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CheckoutShippingAddressBlock class.
*/
class CheckoutShippingAddressBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'checkout-shipping-address-block';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CheckoutShippingMethodBlock class.
*/
class CheckoutShippingMethodBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'checkout-shipping-method-block';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CheckoutShippingMethodsBlock class.
*/
class CheckoutShippingMethodsBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'checkout-shipping-methods-block';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CheckoutTermsBlock class.
*/
class CheckoutTermsBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'checkout-terms-block';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* CheckoutTotalsBlock class.
*/
class CheckoutTotalsBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'checkout-totals-block';
}

View File

@@ -0,0 +1,127 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use WC_Shortcode_Cart;
use WC_Shortcode_Checkout;
use WC_Frontend_Scripts;
/**
* Classic Shortcode class
*
* @internal
*/
class ClassicShortcode extends AbstractDynamicBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'classic-shortcode';
/**
* API version.
*
* @var string
*/
protected $api_version = '3';
/**
* Render method for the Classic Template block. This method will determine which template to render.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block $block Block instance.
* @return string | void Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
if ( ! isset( $attributes['shortcode'] ) ) {
return;
}
/**
* We need to load the scripts here because when using block templates wp_head() gets run after the block
* template. As a result we are trying to enqueue required scripts before we have even registered them.
*
* @see https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues/5328#issuecomment-989013447
*/
if ( class_exists( 'WC_Frontend_Scripts' ) ) {
$frontend_scripts = new WC_Frontend_Scripts();
$frontend_scripts::load_scripts();
}
if ( 'cart' === $attributes['shortcode'] ) {
return $this->render_cart( $attributes );
}
if ( 'checkout' === $attributes['shortcode'] ) {
return $this->render_checkout( $attributes );
}
return "You're using the ClassicShortcode block";
}
/**
* Get the list of classes to apply to this block.
*
* @param array $attributes Block attributes. Default empty array.
* @return string space-separated list of classes.
*/
protected function get_container_classes( $attributes = array() ) {
$classes = array( 'woocommerce', 'wp-block-group' );
if ( isset( $attributes['align'] ) ) {
$classes[] = "align{$attributes['align']}";
}
return implode( ' ', $classes );
}
/**
* Render method for rendering the cart shortcode.
*
* @param array $attributes Block attributes.
* @return string Rendered block type output.
*/
protected function render_cart( $attributes ) {
if ( ! isset( WC()->cart ) ) {
return '';
}
ob_start();
echo '<div class="' . esc_attr( $this->get_container_classes( $attributes ) ) . '">';
WC_Shortcode_Cart::output( array() );
echo '</div>';
return ob_get_clean();
}
/**
* Render method for rendering the checkout shortcode.
*
* @param array $attributes Block attributes.
* @return string Rendered block type output.
*/
protected function render_checkout( $attributes ) {
if ( ! isset( WC()->cart ) ) {
return '';
}
ob_start();
echo '<div class="' . esc_attr( $this->get_container_classes( $attributes ) ) . '">';
WC_Shortcode_Checkout::output( array() );
echo '</div>';
return ob_get_clean();
}
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
}

View File

@@ -0,0 +1,450 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Templates\ProductAttributeTemplate;
use Automattic\WooCommerce\Blocks\Templates\ProductCatalogTemplate;
use Automattic\WooCommerce\Blocks\Templates\ProductCategoryTemplate;
use Automattic\WooCommerce\Blocks\Templates\ProductTagTemplate;
use Automattic\WooCommerce\Blocks\Templates\ProductSearchResultsTemplate;
use Automattic\WooCommerce\Blocks\Templates\OrderConfirmationTemplate;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
use WC_Shortcode_Checkout;
use WC_Frontend_Scripts;
/**
* Classic Template class
*
* @internal
*/
class ClassicTemplate extends AbstractDynamicBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'legacy-template';
/**
* API version.
*
* @var string
*/
protected $api_version = '3';
/**
* Initialize this block.
*/
protected function initialize() {
parent::initialize();
add_filter( 'render_block', array( $this, 'add_alignment_class_to_wrapper' ), 10, 2 );
add_action( 'enqueue_block_assets', array( $this, 'enqueue_block_assets' ) );
}
/**
* 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 );
// Indicate to interactivity powered components that this block is on the page
// and needs refresh to update data.
wp_interactivity_config(
'woocommerce',
array(
'needsRefreshForInteractivityAPI' => true,
)
);
}
/**
* Enqueue assets used for rendering the block in editor context.
*
* This is needed if a block is not yet within the post content--`render` and `enqueue_assets` may not have ran.
*/
public function enqueue_block_assets() {
// Ensures frontend styles for blocks exist in the site editor iframe.
if ( class_exists( 'WC_Frontend_Scripts' ) && is_admin() ) {
$frontend_scripts = new WC_Frontend_Scripts();
$styles = $frontend_scripts::get_styles();
foreach ( $styles as $handle => $style ) {
wp_enqueue_style(
$handle,
set_url_scheme( $style['src'] ),
$style['deps'],
$style['version'],
$style['media']
);
}
}
}
/**
* Enqueue assets specific to this block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block $block Block instance.
*/
protected function enqueue_assets( $attributes, $content, $block ) {
parent::enqueue_assets( $attributes, $content, $block );
if ( is_product() ) {
add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_legacy_assets' ], 20 );
}
}
/**
* Enqueue legacy assets when this block is used as we don't enqueue them for block themes anymore.
*
* Note: This enqueue logic is intentionally duplicated in ProductImageGallery.php
* to keep legacy blocks independent and allow for separate deprecation paths.
*
* @see https://github.com/woocommerce/woocommerce/pull/60223
*/
public function enqueue_legacy_assets() {
// Legacy script dependencies for backward compatibility.
if ( current_theme_supports( 'wc-product-gallery-zoom' ) ) {
wp_enqueue_script( 'wc-zoom' );
}
if ( current_theme_supports( 'wc-product-gallery-slider' ) ) {
wp_enqueue_script( 'wc-flexslider' );
}
if ( current_theme_supports( 'wc-product-gallery-lightbox' ) ) {
wp_enqueue_script( 'wc-photoswipe-ui-default' );
wp_enqueue_style( 'photoswipe-default-skin' );
add_action(
'wp_footer',
function () {
wc_get_template( 'single-product/photoswipe.php' );
}
);
}
wp_enqueue_script( 'wc-single-product' );
}
/**
* Render method for the Classic Template block. This method will determine which template to render.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block $block Block instance.
* @return string | void Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
if ( ! isset( $attributes['template'] ) ) {
return;
}
/**
* We need to load the scripts here because when using block templates wp_head() gets run after the block
* template. As a result we are trying to enqueue required scripts before we have even registered them.
*
* @see https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues/5328#issuecomment-989013447
*/
if ( class_exists( 'WC_Frontend_Scripts' ) ) {
$frontend_scripts = new WC_Frontend_Scripts();
$frontend_scripts::load_scripts();
}
if ( OrderConfirmationTemplate::SLUG === $attributes['template'] ) {
return $this->render_order_received();
}
if ( is_product() ) {
add_filter( 'woocommerce_single_product_zoom_enabled', '__return_true' );
add_filter( 'woocommerce_single_product_photoswipe_enabled', '__return_true' );
add_filter( 'woocommerce_single_product_flexslider_enabled', '__return_true' );
return $this->render_single_product();
}
$valid = false;
$archive_templates = array(
ProductCatalogTemplate::SLUG,
ProductCategoryTemplate::SLUG,
ProductTagTemplate::SLUG,
ProductAttributeTemplate::SLUG,
ProductSearchResultsTemplate::SLUG,
);
// Set selected template when we directly find template base slug.
if ( in_array( $attributes['template'], $archive_templates, true ) ) {
$valid = true;
}
// Set selected template when we find template base slug as prefix for a specific term.
foreach ( $archive_templates as $template ) {
if ( 0 === strpos( $attributes['template'], $template ) ) {
$valid = true;
}
}
if ( $valid ) {
// Set this so that our product filters can detect if it's a PHP template.
$this->asset_data_registry->add( 'isRenderingPhpTemplate', true );
// Set this so filter blocks being used as widgets know when to render.
$this->asset_data_registry->add( 'hasFilterableProducts', true );
$this->asset_data_registry->add(
'pageUrl',
html_entity_decode( get_pagenum_link() )
);
return $this->render_archive_product();
}
ob_start();
echo "You're using the ClassicTemplate block";
wp_reset_postdata();
return ob_get_clean();
}
/**
* Render method for rendering the order confirmation template.
*
* @return string Rendered block type output.
*/
protected function render_order_received() {
ob_start();
echo '<div class="wp-block-group">';
printf(
'<%1$s %2$s>%3$s</%1$s>',
'h1',
get_block_wrapper_attributes(), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
esc_html__( 'Order confirmation', 'woocommerce' )
);
WC_Shortcode_Checkout::output( array() );
echo '</div>';
return ob_get_clean();
}
/**
* Render method for the single product template and parts.
*
* @return string Rendered block type output.
*/
protected function render_single_product() {
ob_start();
/**
* Hook: woocommerce_before_main_content
*
* Called before rendering the main content for a product.
*
* @see woocommerce_output_content_wrapper() Outputs opening DIV for the content (priority 10)
* @see woocommerce_breadcrumb() Outputs breadcrumb trail to the current product (priority 20)
* @see WC_Structured_Data::generate_website_data() Outputs schema markup (priority 30)
*
* @since 6.3.0
*/
do_action( 'woocommerce_before_main_content' );
$product_query = new \WP_Query(
array(
'post_type' => 'product',
'p' => get_the_ID(),
)
);
while ( $product_query->have_posts() ) :
$product_query->the_post();
wc_get_template_part( 'content', 'single-product' );
endwhile;
/**
* Hook: woocommerce_after_main_content
*
* Called after rendering the main content for a product.
*
* @see woocommerce_output_content_wrapper_end() Outputs closing DIV for the content (priority 10)
*
* @since 6.3.0
*/
do_action( 'woocommerce_after_main_content' );
wp_reset_postdata();
return ob_get_clean();
}
/**
* Render method for the archive product template and parts.
*
* @return string Rendered block type output.
*/
protected function render_archive_product() {
ob_start();
/**
* Hook: woocommerce_before_main_content
*
* Called before rendering the main content for a product.
*
* @see woocommerce_output_content_wrapper() Outputs opening DIV for the content (priority 10)
* @see woocommerce_breadcrumb() Outputs breadcrumb trail to the current product (priority 20)
* @see WC_Structured_Data::generate_website_data() Outputs schema markup (priority 30)
*
* @since 6.3.0
*/
do_action( 'woocommerce_before_main_content' );
?>
<header class="woocommerce-products-header">
<?php
/**
* Hook: woocommerce_show_page_title
*
* Allows controlling the display of the page title.
*
* @since 6.3.0
*/
if ( apply_filters( 'woocommerce_show_page_title', true ) ) {
?>
<h1 class="woocommerce-products-header__title page-title">
<?php
woocommerce_page_title();
?>
</h1>
<?php
}
/**
* Hook: woocommerce_archive_description.
*
* @see woocommerce_taxonomy_archive_description() Renders the taxonomy archive description (priority 10)
* @see woocommerce_product_archive_description() Renders the product archive description (priority 10)
*
* @since 6.3.0
*/
do_action( 'woocommerce_archive_description' );
?>
</header>
<?php
if ( woocommerce_product_loop() ) {
/**
* Hook: woocommerce_before_shop_loop.
*
* @see woocommerce_output_all_notices() Render error notices (priority 10)
* @see woocommerce_result_count() Show number of results found (priority 20)
* @see woocommerce_catalog_ordering() Show form to control sort order (priority 30)
*
* @since 6.3.0
*/
do_action( 'woocommerce_before_shop_loop' );
woocommerce_product_loop_start();
if ( wc_get_loop_prop( 'total' ) ) {
while ( have_posts() ) {
the_post();
/**
* Hook: woocommerce_shop_loop.
*
* @since 6.3.0
*/
do_action( 'woocommerce_shop_loop' );
wc_get_template_part( 'content', 'product' );
}
}
woocommerce_product_loop_end();
/**
* Hook: woocommerce_after_shop_loop.
*
* @see woocommerce_pagination() Renders pagination (priority 10)
*
* @since 6.3.0
*/
do_action( 'woocommerce_after_shop_loop' );
} else {
/**
* Hook: woocommerce_no_products_found.
*
* @see wc_no_products_found() Default no products found content (priority 10)
*
* @since 6.3.0
*/
do_action( 'woocommerce_no_products_found' );
}
/**
* Hook: woocommerce_after_main_content
*
* Called after rendering the main content for a product.
*
* @see woocommerce_output_content_wrapper_end() Outputs closing DIV for the content (priority 10)
*
* @since 6.3.0
*/
do_action( 'woocommerce_after_main_content' );
wp_reset_postdata();
return ob_get_clean();
}
/**
* Get HTML markup with the right classes by attributes.
* This function appends the classname at the first element that have the class attribute.
* Based on the experience, all the wrapper elements have a class attribute.
*
* @param string $content Block content.
* @param array $block Parsed block data.
* @return string Rendered block type output.
*/
public function add_alignment_class_to_wrapper( string $content, array $block ) {
if ( ( 'woocommerce/' . $this->block_name ) !== $block['blockName'] ) {
return $content;
}
$attributes = (array) $block['attrs'];
// Set the default alignment to wide.
if ( ! isset( $attributes['align'] ) ) {
$attributes['align'] = 'wide';
}
$align_class_and_style = StyleAttributesUtils::get_align_class_and_style( $attributes );
if ( ! isset( $align_class_and_style['class'] ) ) {
return $content;
}
// Find the first tag.
$first_tag = '<[^<>]+>';
$matches = array();
preg_match( $first_tag, $content, $matches );
// If there is a tag, but it doesn't have a class attribute, add the class attribute.
if ( isset( $matches[0] ) && strpos( $matches[0], ' class=' ) === false ) {
$pattern_before_tag_closing = '/.+?(?=>)/';
return preg_replace( $pattern_before_tag_closing, '$0 class="' . $align_class_and_style['class'] . '"', $content, 1 );
}
// If there is a tag, and it has a class already, add the class attribute.
$pattern_get_class = '/(?<=class=\"|\')[^"|\']+(?=\"|\')/';
return preg_replace( $pattern_get_class, '$0 ' . $align_class_and_style['class'], $content, 1 );
}
}

View File

@@ -0,0 +1,115 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\Jetpack\Constants;
/**
* ComingSoon class.
*/
class ComingSoon extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'coming-soon';
/**
* It is necessary to register and enqueue assets during the render phase because we want to load assets only if the block has the content.
*/
protected function register_block_type_assets() {
parent::register_block_type_assets();
$this->register_chunk_translations( [ $this->block_name ] );
}
/**
* Initialize.
*/
public function initialize() {
parent::initialize();
add_filter( 'enqueue_block_assets', array( $this, 'enqueue_block_assets' ), 10, 2 );
}
/**
* Enqueue frontend assets for this block, just in time for rendering.
*
* @internal This prevents the block script being enqueued on all pages. It is only enqueued as needed. Note that
* we intentionally do not pass 'script' to register_block_type.
*
* @param array $attributes Any attributes that currently are available from the block.
* @param string $content The block content.
* @param WP_Block $block The block object.
*/
protected function enqueue_assets( array $attributes, $content, $block ) {
parent::enqueue_assets( $attributes, $content, $block );
if ( isset( $attributes['style']['color']['background'] ) ) {
wp_add_inline_style(
'wc-blocks-style',
':root{--woocommerce-coming-soon-color: ' . esc_html( $attributes['style']['color']['background'] ) . '}'
);
} elseif ( isset( $attributes['color'] ) ) {
// Deprecated: To support coming soon templates created before WooCommerce 9.8.0.
wp_add_inline_style(
'wc-blocks-style',
':root{--woocommerce-coming-soon-color: ' . esc_html( $attributes['color'] ) . '}'
);
wp_enqueue_style(
'woocommerce-coming-soon',
WC()->plugin_url() . '/assets/css/coming-soon-entire-site-deprecated' . ( is_rtl() ? '-rtl' : '' ) . '.css',
array(),
Constants::get_constant( 'WC_VERSION' )
);
}
}
/**
* Enqueue coming soon deprecated styles in site editor to support
* coming soon templates created before WooCommerce 9.8.0.
*/
public function enqueue_block_assets() {
if ( ! is_admin() ) {
return;
}
$current_screen = get_current_screen();
if ( $current_screen instanceof \WP_Screen && 'site-editor' !== $current_screen->base ) {
return;
}
$post_id = isset( $_REQUEST['postId'] ) ? wc_clean( wp_unslash( $_REQUEST['postId'] ) ) : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( 'woocommerce/woocommerce//coming-soon' !== $post_id ) {
return;
}
$block_template = get_block_template( $post_id );
if ( $block_template ) {
$parsed_blocks = parse_blocks( $block_template->content );
foreach ( $parsed_blocks as $block ) {
if ( isset( $block['blockName'] ) && 'woocommerce/coming-soon' === $block['blockName'] ) {
// Color attribute is deprecated in WooCommerce 9.8.0.
if ( isset( $block['attrs']['color'] ) && ! empty( $block['attrs']['color'] ) ) {
wp_enqueue_style(
'woocommerce-coming-soon',
WC()->plugin_url() . '/assets/css/coming-soon-entire-site-deprecated' . ( is_rtl() ? '-rtl' : '' ) . '.css',
array(),
Constants::get_constant( 'WC_VERSION' )
);
break;
}
}
}
}
}
/**
* 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;
}
}

View File

@@ -0,0 +1,271 @@
<?php
declare(strict_types=1);
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\EmailEditor\Email_Editor_Container;
use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Rendering_Context;
use Automattic\WooCommerce\EmailEditor\Engine\Theme_Controller;
use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Styles_Helper;
use Automattic\WooCommerce\EmailEditor\Integrations\Utils\Table_Wrapper_Helper;
use WP_Block;
/**
* CouponCode block for displaying coupon codes in emails.
*
* @since 10.5.0
*/
class CouponCode extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'coupon-code';
/**
* Default styles for the coupon code element.
*/
private const DEFAULT_STYLES = array(
'font-size' => '1.2em',
'padding' => '12px 20px',
'display' => 'inline-block',
'border' => '2px dashed #cccccc',
'border-radius' => '4px',
'box-sizing' => 'border-box',
'color' => '#000000',
'background-color' => '#f5f5f5',
'text-align' => 'center',
'font-weight' => 'bold',
'letter-spacing' => '1px',
);
/**
* Get the editor script handle for this block type.
*
* @param string|null $key Data to get. Valid keys: "handle", "path", "dependencies".
* @return array|string|null
*/
protected function get_block_type_editor_script( $key = null ) {
$script = array(
'handle' => 'wc-' . $this->block_name . '-block',
'path' => $this->asset_api->get_block_asset_build_path( $this->block_name ),
'dependencies' => array( 'wc-blocks' ),
);
return null === $key ? $script : ( $script[ $key ] ?? null );
}
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
/**
* Render the coupon code block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block|null $block Block instance.
* @return string
*/
protected function render( $attributes, $content, $block ) {
$parsed_block = $block instanceof WP_Block ? $block->parsed_block : array();
$attributes = $this->get_block_attributes( $parsed_block, $attributes );
$coupon_code = $this->get_coupon_code( $attributes );
if ( empty( $coupon_code ) ) {
return '';
}
$rendering_context = $this->get_rendering_context( $block );
$coupon_html = $this->build_coupon_html( $coupon_code, $attributes, $rendering_context );
return $this->wrap_for_email( $coupon_html, $parsed_block );
}
/**
* Get block attributes from parsed block or fallback.
*
* @param array $parsed_block Parsed block data.
* @param array $fallback Fallback attributes.
* @return array
*/
private function get_block_attributes( array $parsed_block, $fallback ): array {
$attributes = $parsed_block['attrs'] ?? $fallback ?? array();
return is_array( $attributes ) ? $attributes : array();
}
/**
* Extract coupon code from attributes.
*
* @param array $attributes Block attributes.
* @return string
*/
private function get_coupon_code( array $attributes ): string {
$coupon_code = $attributes['couponCode'] ?? '';
return is_string( $coupon_code ) ? $coupon_code : '';
}
/**
* Get rendering context from block or create a new one.
*
* @param WP_Block|null $block Block instance.
* @return Rendering_Context
*/
private function get_rendering_context( $block ): Rendering_Context {
if ( $block instanceof WP_Block
&& isset( $block->context['renderingContext'] )
&& $block->context['renderingContext'] instanceof Rendering_Context
) {
return $block->context['renderingContext'];
}
$theme_controller = Email_Editor_Container::container()->get( Theme_Controller::class );
return new Rendering_Context( $theme_controller->get_theme(), array() );
}
/**
* Build the coupon code HTML element with styles.
*
* @param string $coupon_code Coupon code text.
* @param array $attributes Block attributes.
* @param Rendering_Context $rendering_context Rendering context for style resolution.
* @return string
*/
private function build_coupon_html( string $coupon_code, array $attributes, Rendering_Context $rendering_context ): string {
$block_styles = Styles_Helper::get_block_styles(
$attributes,
$rendering_context,
array( 'border', 'background-color', 'color', 'typography', 'spacing' )
);
$declarations = $block_styles['declarations'] ?? array();
if ( ! $this->has_valid_background_color( $declarations ) ) {
$declarations['background-color'] = $this->resolve_background_color( $attributes, $rendering_context );
}
$merged_styles = array_merge( self::DEFAULT_STYLES, $declarations );
$css = \WP_Style_Engine::compile_css( $merged_styles, '' );
return sprintf(
'<span class="woocommerce-coupon-code" style="%s">%s</span>',
esc_attr( $css ),
esc_html( $coupon_code )
);
}
/**
* Check if declarations contain a valid CSS background color.
*
* @param array $declarations CSS declarations.
* @return bool
*/
private function has_valid_background_color( array $declarations ): bool {
if ( empty( $declarations['background-color'] ) ) {
return false;
}
return $this->is_css_color_value( $declarations['background-color'] );
}
/**
* Resolve background color from attributes, translating color slugs if needed.
*
* @param array $attributes Block attributes.
* @param Rendering_Context $rendering_context Rendering context.
* @return string Resolved color value or default.
*/
private function resolve_background_color( array $attributes, Rendering_Context $rendering_context ): string {
if ( empty( $attributes['backgroundColor'] ) ) {
return self::DEFAULT_STYLES['background-color'];
}
$color_slug = $attributes['backgroundColor'];
// Try to get color from normalized styles (handles slug translation).
$normalized = Styles_Helper::get_normalized_block_styles( $attributes, $rendering_context );
$color = $normalized['color']['background'] ?? '';
if ( $this->is_css_color_value( $color ) ) {
return $color;
}
// Fallback: try direct translation if normalization returned the slug unchanged.
$translated = $rendering_context->translate_slug_to_color( $color_slug );
if ( $this->is_css_color_value( $translated ) ) {
return $translated;
}
return self::DEFAULT_STYLES['background-color'];
}
/**
* Check if a string is a valid CSS color value (hex, rgb, or hsl).
*
* @param string $value Value to check.
* @return bool
*/
private function is_css_color_value( string $value ): bool {
return str_starts_with( $value, '#' )
|| str_starts_with( $value, 'rgb' )
|| str_starts_with( $value, 'hsl' );
}
/**
* Wrap coupon HTML in an email-compatible table structure.
*
* @param string $coupon_html Coupon HTML content.
* @param array $parsed_block Parsed block data.
* @return string
*/
private function wrap_for_email( string $coupon_html, array $parsed_block ): string {
$align = $this->get_alignment( $parsed_block );
$table_attrs = array(
'style' => \WP_Style_Engine::compile_css(
array(
'border-collapse' => 'collapse',
'width' => '100%',
),
''
),
'width' => '100%',
);
$cell_attrs = array(
'class' => 'email-coupon-code-cell',
'style' => \WP_Style_Engine::compile_css(
array(
'padding' => '10px 0',
'text-align' => $align,
),
''
),
'align' => $align,
);
return Table_Wrapper_Helper::render_table_wrapper( $coupon_html, $table_attrs, $cell_attrs );
}
/**
* Get alignment from parsed block attributes.
*
* @param array $parsed_block Parsed block data.
* @return string
*/
private function get_alignment( array $parsed_block ): string {
$allowed = array( 'left', 'center', 'right' );
$align = $parsed_block['attrs']['align'] ?? 'center';
if ( ! is_string( $align ) || ! in_array( $align, $allowed, true ) ) {
return 'center';
}
return $align;
}
}

View File

@@ -0,0 +1,234 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
use Automattic\WooCommerce\Blocks\Utils\BlockHooksTrait;
/**
* CustomerAccount class.
*/
class CustomerAccount extends AbstractBlock {
use BlockHooksTrait;
const TEXT_ONLY = 'text_only';
const ICON_ONLY = 'icon_only';
const DISPLAY_ALT = 'alt';
const DISPLAY_LINE = 'line';
/**
* Block name.
*
* @var string
*/
protected $block_name = 'customer-account';
/**
* Block Hook API placements.
*
* @var array
*/
protected $hooked_block_placements = array(
array(
'position' => 'after',
'anchor' => 'core/navigation',
'area' => 'header',
'callback' => 'should_unhook_block',
'version' => '8.4.0',
),
);
/**
* Initialize this block type.
*/
protected function initialize() {
parent::initialize();
/**
* The hooked_block_{$hooked_block_type} filter was added in WordPress 6.5.
* We are the only code adding the filter 'hooked_block_woocommerce/customer-account'.
* Using has_filter() for a compatibility check won't work because add_filter() is used in the same file.
*/
if ( version_compare( get_bloginfo( 'version' ), '6.5', '>=' ) ) {
add_filter( 'hooked_block_woocommerce/customer-account', array( $this, 'modify_hooked_block_attributes' ), 10, 5 );
add_filter( 'hooked_block_types', array( $this, 'register_hooked_block' ), 9, 4 );
}
}
/**
* Callback for the Block Hooks API to modify the attributes of the hooked block.
*
* @param array|null $parsed_hooked_block The parsed block array for the given hooked block type, or null to suppress the block.
* @param string $hooked_block_type The hooked block type name.
* @param string $relative_position The relative position of the hooked block.
* @param array $parsed_anchor_block The anchor block, in parsed block array format.
* @param WP_Block_Template|WP_Post|array $context The block template, template part, `wp_navigation` post type,
* or pattern that the anchor block belongs to.
* @return array|null
*/
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';
$customer_account_block_font_size = wp_get_global_styles( array( 'blocks', 'woocommerce/customer-account', 'typography', 'fontSize' ) );
if ( ! is_string( $customer_account_block_font_size ) ) {
$navigation_block_font_size = wp_get_global_styles( array( 'blocks', 'core/navigation', 'typography', 'fontSize' ) );
if ( is_string( $navigation_block_font_size ) ) {
$parsed_hooked_block['attrs']['style']['typography']['fontSize'] = $navigation_block_font_size;
}
}
return $parsed_hooked_block;
}
/**
* Callback for the Block Hooks API to determine if the block should be auto-inserted.
*
* @param array $hooked_blocks An array of block slugs hooked into a given context.
* @param string $position Position of the block insertion point.
* @param string $anchor_block The block acting as the anchor for the inserted block.
* @param array|\WP_Post|\WP_Block_Template $context Where the block is embedded.
*
* @return array
*/
protected function should_unhook_block( $hooked_blocks, $position, $anchor_block, $context ) {
$block_name = $this->namespace . '/' . $this->block_name;
$block_is_hooked = in_array( $block_name, $hooked_blocks, true );
if ( $block_is_hooked ) {
$active_theme = wp_get_theme()->get( 'Name' );
$exclude_themes = array( 'Twenty Twenty-Two', 'Twenty Twenty-Three' );
if ( in_array( $active_theme, $exclude_themes, true ) ) {
$key = array_search( $block_name, $hooked_blocks, true );
unset( $hooked_blocks[ $key ] );
}
}
return $hooked_blocks;
}
/**
* Render the block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block $block Block instance.
*
* @return string Rendered block output.
*/
protected function render( $attributes, $content, $block ) {
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
$account_link = get_option( 'woocommerce_myaccount_page_id' ) ? wc_get_account_endpoint_url( 'dashboard' ) : wp_login_url();
$allowed_svg = array(
'svg' => array(
'class' => true,
'xmlns' => true,
'width' => true,
'height' => true,
'viewbox' => true,
),
'path' => array(
'd' => true,
'fill' => true,
'fill-rule' => true,
'clip-rule' => true,
),
'circle' => array(
'cx' => true,
'cy' => true,
'r' => true,
'stroke' => true,
'stroke-width' => true,
'fill' => true,
),
);
// Only provide aria-label if the display style is icon only.
$aria_label = self::ICON_ONLY === $attributes['displayStyle'] ? 'aria-label="' . esc_attr( $this->render_label() ) . '"' : '';
$label_markup = self::ICON_ONLY === $attributes['displayStyle'] ? '' : '<span class="label">' . wp_kses( $this->render_label(), array() ) . '</span>';
return "<div class='wp-block-woocommerce-customer-account " . esc_attr( $classes_and_styles['classes'] ) . "' style='" . esc_attr( $classes_and_styles['styles'] ) . "'>
<a " . $aria_label . " href='" . esc_attr( $account_link ) . "'>
" . wp_kses( $this->render_icon( $attributes ), $allowed_svg ) . $label_markup . '
</a>
</div>';
}
/**
* Gets the icon to render depending on the iconStyle and displayStyle.
*
* @param array $attributes Block attributes.
*
* @return string Label to render on the block
*/
private function render_icon( $attributes ) {
if ( self::TEXT_ONLY === $attributes['displayStyle'] ) {
return '';
}
if ( self::DISPLAY_LINE === $attributes['iconStyle'] ) {
return '<svg class="' . $attributes['iconClass'] . '" viewBox="1 1 29 29" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle
cx="16"
cy="10.5"
r="3.5"
stroke="currentColor"
stroke-width="2"
fill="none"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M11.5 18.5H20.5C21.8807 18.5 23 19.6193 23 21V25.5H25V21C25 18.5147 22.9853 16.5 20.5 16.5H11.5C9.01472 16.5 7 18.5147 7 21V25.5H9V21C9 19.6193 10.1193 18.5 11.5 18.5Z"
fill="currentColor"
/>
</svg>';
}
if ( self::DISPLAY_ALT === $attributes['iconStyle'] ) {
return '<svg class="' . $attributes['iconClass'] . '" xmlns="http://www.w3.org/2000/svg" viewBox="-4 -4 25 25">
<path
d="M9 0C4.03579 0 0 4.03579 0 9C0 13.9642 4.03579 18 9 18C13.9642 18 18 13.9642 18 9C18 4.03579 13.9642 0 9 0ZM9 4.32C10.5347 4.32 11.7664 5.57056 11.7664 7.08638C11.7664 8.62109 10.5158 9.85277 9 9.85277C7.4653 9.85277 6.23362 8.60221 6.23362 7.08638C6.23362 5.57056 7.46526 4.32 9 4.32ZM9 10.7242C11.1221 10.7242 12.96 12.2021 13.7937 14.4189C12.5242 15.5559 10.8379 16.238 9 16.238C7.16207 16.238 5.49474 15.5369 4.20632 14.4189C5.05891 12.2021 6.87793 10.7242 9 10.7242Z"
fill="currentColor"
/>
</svg>';
}
return '<svg class="' . $attributes['iconClass'] . '" xmlns="http://www.w3.org/2000/svg" viewBox="-5 -5 25 25">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8.00009 8.34785C10.3096 8.34785 12.1819 6.47909 12.1819 4.17393C12.1819 1.86876 10.3096 0 8.00009 0C5.69055 0 3.81824 1.86876 3.81824 4.17393C3.81824 6.47909 5.69055 8.34785 8.00009 8.34785ZM0.333496 15.6522C0.333496 15.8444 0.489412 16 0.681933 16H15.3184C15.5109 16 15.6668 15.8444 15.6668 15.6522V14.9565C15.6668 12.1428 13.7821 9.73911 10.0912 9.73911H5.90931C2.21828 9.73911 0.333645 12.1428 0.333645 14.9565L0.333496 15.6522Z"
fill="currentColor"
/>
</svg>';
}
/**
* Gets the label to render depending on the displayStyle.
*
* @return string Label to render on the block.
*/
private function render_label() {
return get_current_user_id()
? __( 'My Account', 'woocommerce' )
: __( 'Login', 'woocommerce' );
}
/**
* Get the frontend script handle for this block type.
*
* @param string $key Data to get, or default to everything.
*
* @return null This block has no frontend script.
*/
protected function get_block_type_script( $key = null ) {
return null;
}
}

View File

@@ -0,0 +1,102 @@
<?php // phpcs:ignore Generic.PHP.RequireStrictTypes.MissingDeclaration
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\BlockTypes\AbstractBlock;
use Automattic\WooCommerce\Internal\EmailEditor\BlockEmailRenderer;
use Automattic\WooCommerce\Internal\Admin\EmailPreview\EmailPreview;
use Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails\WCTransactionalEmailPostsManager;
/**
* EmailContent class.
*/
class EmailContent extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'email-content';
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
/**
* Get the editor script handle for this block type.
*
* @param string $key Data to get, or default to everything.
* @return array|string
*/
protected function get_block_type_editor_script( $key = null ) {
$script = [
'handle' => 'wc-' . $this->block_name . '-block',
'path' => $this->asset_api->get_block_asset_build_path( $this->block_name ),
'dependencies' => [ 'wc-blocks' ],
];
return $key ? $script[ $key ] : $script;
}
/**
* Get the frontend script handle for this block type.
*
* @param string $key Data to get, or default to everything.
*/
protected function get_block_type_script( $key = null ) {
return null;
}
/**
* Renders the block preview for the editor.
*
* @param array $attributes Block attributes.
* @return string Rendered block output.
*/
protected function render_preview( $attributes ) {
/**
* Email preview instance for rendering dummy content.
*
* @var EmailPreview $email_preview - email preview instance
*/
$email_preview = wc_get_container()->get( EmailPreview::class );
$type_param = EmailPreview::DEFAULT_EMAIL_TYPE;
if ( isset( $attributes['postId'] ) ) {
$email_type_class_name = WCTransactionalEmailPostsManager::get_instance()->get_email_type_class_name_from_post_id( $attributes['postId'] );
$type_param = ! empty( $email_type_class_name ) ? $email_type_class_name : $type_param;
} elseif ( isset( $attributes['emailType'] ) ) {
$type_param = sanitize_text_field( wp_unslash( $attributes['emailType'] ) );
}
try {
return $email_preview->generate_placeholder_content( $type_param );
} catch ( \Exception $e ) {
// Catch other potential errors during content generation.
return esc_html__( 'There was an error rendering the email preview.', 'woocommerce' );
}
}
/**
* Renders Woo content placeholder to be replaced by content during sending.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param \WP_Block $block Block instance.
* @return string Rendered block output.
*/
protected function render( $attributes, $content, $block ) {
// phpcs:disable WordPress.Security.NonceVerification.Recommended
if ( defined( 'REST_REQUEST' ) && REST_REQUEST && isset( $_GET['context'] ) && 'edit' === sanitize_text_field( wp_unslash( $_GET['context'] ) ) ) {
// Block is being rendered for ServerSideRender editor preview.
return $this->render_preview( $attributes );
}
return BlockEmailRenderer::WOO_EMAIL_CONTENT_PLACEHOLDER;
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* EmptyCartBlock class.
*/
class EmptyCartBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'empty-cart-block';
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Admin\Features\Features;
/**
* EmptyMiniCartContentsBlock class.
*/
class EmptyMiniCartContentsBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'empty-mini-cart-contents-block';
/**
* Render the markup for the Filled Mini-Cart Contents 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 ( Features::is_enabled( 'experimental-iapi-mini-cart' ) ) {
return $this->render_experimental_empty_mini_cart_contents( $attributes, $content, $block );
}
return $content;
}
/**
* Render the experimental interactivity API powered Filled Mini-Cart Contents 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_experimental_empty_mini_cart_contents( $attributes, $content, $block ) {
$wrapper_attributes = get_block_wrapper_attributes(
array(
'data-wp-bind--aria-hidden' => '!state.cartIsEmpty',
'data-wp-bind--hidden' => '!state.cartIsEmpty',
'data-wp-interactive' => 'woocommerce/mini-cart',
)
);
ob_start();
?>
<div <?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
<div class="wc-block-mini-cart__empty-cart-wrapper">
<?php
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $content;
?>
</div>
</div>
<?php
return ob_get_clean();
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\Blocks\BlockTypes;
trait EnableBlockJsonAssetsTrait {
/**
* Disable the script handle for this block type. We use block.json to load the script.
*
* @param string|null $key The key of the script to get.
* @return null
*/
// phpcs:ignore
protected function get_block_type_script( $key = null ) {
return null;
}
/**
* Disable the style handle for this block type. We use block.json to load the style.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
/**
* Disable the editor style handle for this block type. We use block.json to load the style.
*
* @return null
*/
protected function get_block_type_editor_style() {
return null;
}
}

View File

@@ -0,0 +1,112 @@
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* FeaturedCategory class.
*/
class FeaturedCategory extends FeaturedItem {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'featured-category';
/**
* Get block attributes.
*
* @return array
*/
protected function get_block_type_attributes() {
return array_merge(
parent::get_block_type_attributes(),
array(
'textColor' => $this->get_schema_string(),
'fontSize' => $this->get_schema_string(),
'lineHeight' => $this->get_schema_string(),
'style' => array( 'type' => 'object' ),
)
);
}
/**
* Returns the featured category.
*
* @param array $attributes Block attributes. Default empty array.
* @return \WP_Term|null
*/
protected function get_item( $attributes ) {
$id = absint( $attributes['categoryId'] ?? 0 );
$category = get_term( $id, 'product_cat' );
if ( ! $category || is_wp_error( $category ) ) {
return null;
}
return $category;
}
/**
* Returns the name of the featured category.
*
* @param \WP_Term $category Featured category.
* @return string
*/
protected function get_item_title( $category ) {
return $category->name;
}
/**
* Returns the featured category image URL.
*
* @param \WP_Term $category Term object.
* @param string $size Image size, defaults to 'full'.
* @return string
*/
protected function get_item_image( $category, $size = 'full' ) {
$image = '';
$image_id = get_term_meta( $category->term_id, 'thumbnail_id', true );
if ( $image_id ) {
$image = wp_get_attachment_image_url( $image_id, $size );
}
return $image;
}
/**
* Renders the featured category attributes.
*
* @param \WP_Term $category Term object.
* @param array $attributes Block attributes. Default empty array.
* @return string
*/
protected function render_attributes( $category, $attributes ) {
$output = '';
// Backwards compatibility: Only render legacy attributes if `editMode` exists as boolean value
// This allows us to distinguish between old and new version of the block (which accept inner blocks).
if ( array_key_exists( 'editMode', $attributes ) && is_bool( $attributes['editMode'] ) ) {
$legacy_title = sprintf(
'<h2 class="wc-block-featured-category__title">%s</h2>',
wp_kses_post( $category->name )
);
$output .= $legacy_title;
if (
! isset( $attributes['showDesc'] ) ||
( isset( $attributes['showDesc'] ) && false !== $attributes['showDesc'] )
) {
$desc_str = sprintf(
'<div class="wc-block-featured-category__description">%s</div>',
wc_format_content( wp_kses_post( $category->description ) )
);
$output .= $desc_str;
}
}
return $output;
}
}

View File

@@ -0,0 +1,477 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
/**
* FeaturedItem class.
*/
abstract class FeaturedItem extends AbstractDynamicBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name;
/**
* Default attribute values.
*
* @var array
*/
protected $defaults = array(
'align' => 'none',
);
/**
* Global style enabled for this block.
*
* @var array
*/
protected $global_style_wrapper = array(
'background_color',
'border_color',
'border_radius',
'border_width',
'font_size',
'padding',
'text_color',
'extra_classes',
);
/**
* Initialize the block.
*/
protected function initialize() {
parent::initialize();
add_filter( 'render_block_context', [ $this, 'update_context' ], 10, 3 );
add_filter( 'render_block_core/post-title', [ $this, 'restore_global_post' ], 10, 3 );
}
/**
* Current item (product or category) for context
*
* @var \WP_Term|\WC_Product|null
*/
private $current_item = null;
/**
* Current featured item ID (product or category) for context
*
* @var int
*/
protected $featured_item_id = 0;
/**
* Featured Item inner blocks names.
* This is used to map all the inner blocks for a Featured Item block.
*
* @var array
*/
protected $featured_item_inner_blocks_names = [];
/**
* Extract the inner block names for the Featured Item block. This way it's possible
* to map all the inner blocks for a Featured Item block and manipulate the data as needed.
*
* @param array $block The Featured Item block or its inner blocks.
* @param array $result Array of inner block names.
*
* @return array Array containing all the inner block names of a Featured Item block.
*/
protected function extract_featured_item_inner_block_names( $block, &$result = [] ) {
if ( isset( $block['blockName'] ) ) {
$result[] = $block['blockName'];
}
if ( 'woocommerce/product-template' === $block['blockName'] || 'core/post-template' === $block['blockName'] ) {
return $result;
}
if ( isset( $block['innerBlocks'] ) ) {
foreach ( $block['innerBlocks'] as $inner_block ) {
$this->extract_featured_item_inner_block_names( $inner_block, $result );
}
}
return $result;
}
/**
* Replace the global post for the Featured Item inner blocks and reset it after.
*
* This is needed because some of the inner blocks may use the global post
* instead of fetching the product through the context, so even if the
* context is passed to the inner block, it will still use the global post.
*
* @param array $block Block attributes.
* @param array $context Block context.
*/
protected function replace_post_for_featured_item_inner_block( $block, &$context ) {
if ( $this->featured_item_inner_blocks_names ) {
$block_name = end( $this->featured_item_inner_blocks_names );
if ( $block_name === $block['blockName'] ) {
array_pop( $this->featured_item_inner_blocks_names );
// Handle core blocks that need global post manipulation.
if ( 'core/post-excerpt' === $block_name || 'core/post-title' === $block_name ) {
global $post;
$post = get_post( $this->featured_item_id ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
if ( $post instanceof \WP_Post ) {
setup_postdata( $post );
}
}
$context['postId'] = $this->featured_item_id;
$context['postType'] = 'product';
$this->current_item = wc_get_product( $this->featured_item_id );
}
}
}
/**
* Update context for inner blocks to provide postId and postType.
*
* @param array $context Block context.
* @param array $parsed_block Block attributes.
* @param WP_Block $parent_block Block instance.
*
* @return array Updated block context.
*/
public function update_context( $context, $parsed_block, $parent_block ) {
// Check if this is a featured item block and extract all inner block names.
if ( ( 'woocommerce/featured-product' === $parsed_block['blockName'] || 'woocommerce/featured-category' === $parsed_block['blockName'] )
&& isset( $parsed_block['attrs'] ) ) {
$item = $this->get_item( $parsed_block['attrs'] );
if ( $item instanceof \WC_Product ) {
$this->featured_item_id = $item->get_id();
$this->featured_item_inner_blocks_names = array_reverse(
$this->extract_featured_item_inner_block_names( $parsed_block )
);
}
}
// Replace post context for featured item inner blocks.
$this->replace_post_for_featured_item_inner_block( $parsed_block, $context );
return $context;
}
/**
* Restore global post data after rendering core/post-title.
*
* @param string $block_content The block content.
* @param array $parsed_block The full block, including name and attributes.
* @param \WP_Block $block_instance The block instance.
*
* @return string
*/
public function restore_global_post( $block_content, $parsed_block, $block_instance ) {
if ( $this->current_item ) {
wp_reset_postdata();
}
return $block_content;
}
/**
* Returns the featured item.
*
* @param array $attributes Block attributes. Default empty array.
* @return \WP_Term|\WC_Product|null
*/
abstract protected function get_item( $attributes );
/**
* Returns the name of the featured item.
*
* @param \WP_Term|\WC_Product $item Item object.
* @return string
*/
abstract protected function get_item_title( $item );
/**
* Returns the featured item image URL.
*
* @param \WP_Term|\WC_Product $item Item object.
* @param string $size Image size, defaults to 'full'.
* @return string
*/
abstract protected function get_item_image( $item, $size = 'full' );
/**
* Renders the featured item attributes.
*
* @param \WP_Term|\WC_Product $item Item object.
* @param array $attributes Block attributes. Default empty array.
* @return string
*/
abstract protected function render_attributes( $item, $attributes );
/**
* Render the featured item 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 ) {
$item = $this->get_item( $attributes );
if ( ! $item ) {
return '';
}
$aria_label = $attributes['ariaLabel'] ?? '';
$attributes = wp_parse_args( $attributes, $this->defaults );
$attributes['height'] = $attributes['height'] ?? wc_get_theme_support( 'featured_block::default_height', 500 );
$image_url = esc_url( $this->get_image_url( $attributes, $item ) );
$styles = $this->get_styles( $attributes );
$classes = $this->get_classes( $attributes );
$output = sprintf( '<div class="%1$s wp-block-woocommerce-%2$s" style="%3$s">', esc_attr( trim( $classes ) ), $this->block_name, esc_attr( $styles ) );
$output .= sprintf( '<div class="wc-block-%s__wrapper">', $this->block_name );
$output .= $this->render_overlay( $attributes );
if ( ! $attributes['isRepeated'] && ! $attributes['hasParallax'] ) {
$output .= $this->render_image( $attributes, $item, $image_url );
} else {
$output .= $this->render_bg_image( $attributes, $image_url );
}
if ( isset( $aria_label ) && ! empty( $aria_label ) ) {
$p = new \WP_HTML_Tag_Processor( $content );
if ( $p->next_tag( 'a', [ 'class' => 'wp-block-button__link' ] ) ) {
$p->set_attribute( 'aria-label', $aria_label );
$content = $p->get_updated_html();
}
}
// Render additional attributes (e.g. description/price) for legacy compatibility.
$output .= $this->render_attributes( $item, $attributes );
if ( ! empty( $content ) ) {
$output .= sprintf( '<div class="wc-block-%s__inner-blocks">%s</div>', $this->block_name, $content );
}
$output .= '</div>';
$output .= '</div>';
return $output;
}
/**
* Returns the url the item's image
*
* @param array $attributes Block attributes. Default empty array.
* @param \WP_Term|\WC_Product $item Item object.
*
* @return string
*/
private function get_image_url( $attributes, $item ) {
$image_size = 'large';
if ( 'none' !== $attributes['align'] || $attributes['height'] > 800 ) {
$image_size = 'full';
}
if ( $attributes['mediaId'] ) {
return wp_get_attachment_image_url( $attributes['mediaId'], $image_size );
}
return $this->get_item_image( $item, $image_size );
}
/**
* Renders the featured image as a div background.
*
* @param array $attributes Block attributes. Default empty array.
* @param string $image_url Item image url.
*
* @return string
*/
private function render_bg_image( $attributes, $image_url ) {
$styles = $this->get_bg_styles( $attributes, $image_url );
$classes = [ "wc-block-{$this->block_name}__background-image" ];
if ( $attributes['hasParallax'] ) {
$classes[] = ' has-parallax';
}
return sprintf( '<div class="%1$s" style="%2$s" /></div>', esc_attr( implode( ' ', $classes ) ), esc_attr( $styles ) );
}
/**
* Get the styles for the wrapper element (background image, color).
*
* @param array $attributes Block attributes. Default empty array.
* @param string $image_url Item image url.
*
* @return string
*/
public function get_bg_styles( $attributes, $image_url ) {
$style = '';
if ( $attributes['isRepeated'] || $attributes['hasParallax'] ) {
$style .= "background-image: url($image_url);";
}
if ( ! $attributes['isRepeated'] ) {
$style .= 'background-repeat: no-repeat;';
$bg_size = 'cover' === $attributes['imageFit'] ? $attributes['imageFit'] : 'auto';
$style .= 'background-size: ' . $bg_size . ';';
}
if ( $this->hasFocalPoint( $attributes ) ) {
$style .= sprintf(
'background-position: %s%% %s%%;',
$attributes['focalPoint']['x'] * 100,
$attributes['focalPoint']['y'] * 100
);
}
$global_style_style = StyleAttributesUtils::get_styles_by_attributes( $attributes, $this->global_style_wrapper );
$style .= $global_style_style;
return $style;
}
/**
* Renders the featured image
*
* @param array $attributes Block attributes. Default empty array.
* @param \WC_Product|\WP_Term $item Item object.
* @param string $image_url Item image url.
*
* @return string
*/
private function render_image( $attributes, $item, string $image_url ) {
$style = sprintf( 'object-fit: %s;', esc_attr( $attributes['imageFit'] ) );
$img_alt = $attributes['alt'] ?: $this->get_item_title( $item );
if ( $this->hasFocalPoint( $attributes ) ) {
$style .= sprintf(
'object-position: %s%% %s%%;',
$attributes['focalPoint']['x'] * 100,
$attributes['focalPoint']['y'] * 100
);
}
if ( ! empty( $image_url ) ) {
return sprintf(
'<img alt="%1$s" class="wc-block-%2$s__background-image" src="%3$s" style="%4$s" />',
esc_attr( $img_alt ),
$this->block_name,
esc_url( $image_url ),
esc_attr( $style )
);
}
return '';
}
/**
* Get the styles for the wrapper element (background image, color).
*
* @param array $attributes Block attributes. Default empty array.
* @return string
*/
public function get_styles( $attributes ) {
$style = '';
$min_height = $attributes['minHeight'] ?? wc_get_theme_support( 'featured_block::default_height', 500 );
if ( isset( $attributes['minHeight'] ) ) {
$style .= sprintf( 'min-height:%dpx;', intval( $min_height ) );
}
$global_style_style = StyleAttributesUtils::get_styles_by_attributes( $attributes, $this->global_style_wrapper );
$style .= $global_style_style;
return $style;
}
/**
* Get class names for the block container.
*
* @param array $attributes Block attributes. Default empty array.
* @return string
*/
public function get_classes( $attributes ) {
$classes = array( 'wc-block-' . $this->block_name );
if ( isset( $attributes['align'] ) ) {
$classes[] = "align{$attributes['align']}";
}
if ( isset( $attributes['dimRatio'] ) && ( 0 !== $attributes['dimRatio'] ) ) {
$classes[] = 'has-background-dim';
if ( 50 !== $attributes['dimRatio'] ) {
$classes[] = 'has-background-dim-' . 10 * round( $attributes['dimRatio'] / 10 );
}
}
if ( isset( $attributes['contentAlign'] ) && 'center' !== $attributes['contentAlign'] ) {
$classes[] = "has-{$attributes['contentAlign']}-content";
}
$global_style_classes = StyleAttributesUtils::get_classes_by_attributes( $attributes, $this->global_style_wrapper );
$classes[] = $global_style_classes;
return implode( ' ', $classes );
}
/**
* Renders the block overlay
*
* @param array $attributes Block attributes. Default empty array.
*
* @return string
*/
private function render_overlay( $attributes ) {
if ( isset( $attributes['overlayGradient'] ) ) {
$overlay_styles = sprintf( 'background-image: %s', $attributes['overlayGradient'] );
} elseif ( isset( $attributes['overlayColor'] ) ) {
$overlay_styles = sprintf( 'background-color: %s', $attributes['overlayColor'] );
} else {
$overlay_styles = 'background-color: #000000';
}
return sprintf( '<div class="background-dim__overlay" style="%s"></div>', esc_attr( $overlay_styles ) );
}
/**
* Returns whether the focal point is defined for the block.
*
* @param array $attributes Block attributes. Default empty array.
*
* @return bool
*/
private function hasFocalPoint( $attributes ): bool {
return is_array( $attributes['focalPoint'] ) && 2 === count( $attributes['focalPoint'] );
}
/**
* 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 = [] ) {
parent::enqueue_data( $attributes );
$this->asset_data_registry->add( 'defaultHeight', wc_get_theme_support( 'featured_block::default_height', 500 ) );
}
}

View File

@@ -0,0 +1,118 @@
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Enums\ProductStatus;
use Automattic\WooCommerce\Enums\ProductType;
/**
* FeaturedProduct class.
*/
class FeaturedProduct extends FeaturedItem {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'featured-product';
/**
* Returns the featured product.
*
* @param array $attributes Block attributes. Default empty array.
* @return \WP_Term|null
*/
protected function get_item( $attributes ) {
$id = absint( $attributes['productId'] ?? 0 );
$product = wc_get_product( $id );
if ( ! $product || ( ProductStatus::PUBLISH !== $product->get_status() && ! current_user_can( 'read_product', $id ) ) ) {
return null;
}
return $product;
}
/**
* Returns the name of the featured product.
*
* @param \WC_Product $product Product object.
* @return string
*/
protected function get_item_title( $product ) {
return $product->get_title();
}
/**
* Returns the featured product image URL.
*
* @param \WC_Product $product Product object.
* @param string $size Image size, defaults to 'full'.
* @return string
*/
protected function get_item_image( $product, $size = 'full' ) {
$image = '';
if ( $product->get_image_id() ) {
$image = wp_get_attachment_image_url( $product->get_image_id(), $size );
} elseif ( $product->get_parent_id() ) {
$parent_product = wc_get_product( $product->get_parent_id() );
if ( $parent_product ) {
$image = wp_get_attachment_image_url( $parent_product->get_image_id(), $size );
}
}
return $image;
}
/**
* Renders the featured product attributes.
*
* @param \WC_Product $product Product object.
* @param array $attributes Block attributes. Default empty array.
* @return string
*/
protected function render_attributes( $product, $attributes ) {
$output = '';
// Backwards compatibility: Only render legacy attributes if `editMode` exists as boolean value
// This allows us to distinguish between old and new version of the block (which accept inner blocks).
if ( array_key_exists( 'editMode', $attributes ) && is_bool( $attributes['editMode'] ) ) {
$legacy_title = sprintf(
'<h2 class="wc-block-featured-product__title">%s</h2>',
wp_kses_post( $product->get_title() )
);
if ( $product->is_type( ProductType::VARIATION ) ) {
$legacy_title .= sprintf(
'<h3 class="wc-block-featured-product__variation">%s</h3>',
wp_kses_post( wc_get_formatted_variation( $product, true, true, false ) )
);
}
$output .= $legacy_title;
if (
! isset( $attributes['showDesc'] ) ||
( isset( $attributes['showDesc'] ) && false !== $attributes['showDesc'] )
) {
$desc_str = sprintf(
'<div class="wc-block-featured-product__description">%s</div>',
wc_format_content( wp_kses_post( $product->get_short_description() ? $product->get_short_description() : wc_trim_string( $product->get_description(), 400 ) ) )
);
$output .= $desc_str;
}
if (
! isset( $attributes['showPrice'] ) ||
( isset( $attributes['showPrice'] ) && false !== $attributes['showPrice'] )
) {
$price_str = sprintf(
'<div class="wc-block-featured-product__price">%s</div>',
wp_kses_post( $product->get_price_html() )
);
$output .= $price_str;
}
}
return $output;
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* FilledCartBlock class.
*/
class FilledCartBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'filled-cart-block';
}

View File

@@ -0,0 +1,105 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Admin\Features\Features;
/**
* FilledMiniCartContentsBlock class.
*/
class FilledMiniCartContentsBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'filled-mini-cart-contents-block';
/**
* Render the markup for the Filled Mini-Cart Contents 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 ( Features::is_enabled( 'experimental-iapi-mini-cart' ) ) {
return $this->render_experimental_filled_mini_cart_contents( $attributes, $content, $block );
}
return $content;
}
/**
* Render the experimental interactivity API powered Filled Mini-Cart Contents 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_experimental_filled_mini_cart_contents( $attributes, $content, $block ) {
$context = wp_json_encode(
array(
'notices' => array(),
),
JSON_NUMERIC_CHECK
| JSON_HEX_TAG
| JSON_HEX_APOS
| JSON_HEX_QUOT
| JSON_HEX_AMP
);
$wrapper_attributes = get_block_wrapper_attributes(
array(
'data-wp-interactive' => 'woocommerce/mini-cart',
'data-wp-context' => 'woocommerce/store-notices::' . $context,
'data-wp-bind--hidden' => 'state.cartIsEmpty',
)
);
ob_start();
?>
<div <?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
<div
class="wc-block-components-notices"
data-wp-interactive="woocommerce/store-notices"
><template
data-wp-each--notice="context.notices"
data-wp-each-key="context.notice.id"
>
<div
class="wc-block-components-notice-banner"
data-wp-class--is-error="state.isError"
data-wp-class--is-success ="state.isSuccess"
data-wp-class--is-info="state.isInfo"
data-wp-class--is-dismissible="context.notice.dismissible"
data-wp-bind--role="state.role"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false">
<path data-wp-bind--d="state.iconPath"></path>
</svg>
<div class="wc-block-components-notice-banner__content">
<span data-wp-init="callbacks.renderNoticeContent"></span>
</div>
<button
data-wp-bind--hidden="!context.notice.dismissible"
class="wc-block-components-button wp-element-button wc-block-components-notice-banner__dismiss contained"
aria-label="<?php esc_attr_e( 'Dismiss this notice', 'woocommerce' ); ?>"
data-wp-on--click="actions.removeNotice"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<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" />
</svg>
</button>
</div>
</template></div>
<?php
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $content;
?>
</div>
<?php
return ob_get_clean();
}
}

Some files were not shown because too many files have changed in this diff Show More