'woocommerce_post_class', 'args' => 2 ] ]; private $initial_woo_behavior = [ 'product_title' => true, 'product_rating' => true, 'product_price' => true, 'product_desc' => true, 'product_meta' => true, 'product_sharing' => true, // For now detect only someone disabled the add to cart // from the summary. 'product_add_to_cart' => true ]; public function __construct() { $this->additional_actions = new SingleProductAdditionalActions(); new WooCommerceAddToCart(); $this->attach_hooks(); add_filter('the_password_form', function($output, $post) { if ($post->post_type !== 'product') { return $output; } return str_replace( 'post-password-form"', 'post-password-form is-width-constrained"', $output ); }, 10, 2); add_action('wp_enqueue_scripts', function() { if ( get_option('woocommerce_enable_ajax_add_to_cart') === 'no' && blocksy_manager()->screen->is_product() ) { wp_enqueue_script('wc-add-to-cart'); } }); } // The single generic entry point for single-product post classes — // applied once per product per request, no matter how many times // WooCommerce fires `woocommerce_post_class` for it (quick view, AJAX // re-renders, plugins re-rendering the wrapper). public function woocommerce_post_class($classes, $product) { if (! $product instanceof \WC_Product) { return $classes; } if ( ! blocksy_manager()->screen->is_product() && ! wp_doing_ajax() ) { return $classes; } // Loops rendered on the single product page (related products, // upsells) fire `woocommerce_post_class` too — the filter is only // for the main product. Non-product queried objects (e.g. a page // with the `[product_page]` shortcode) are exempt from the check. $queried_object = get_queried_object(); if ( $queried_object instanceof \WP_Post && $queried_object->post_type === 'product' && (int) $queried_object->ID !== (int) $product->get_id() ) { return $classes; } if (in_array( $product->get_id(), $this->post_class_handled_product_ids )) { return $classes; } $this->post_class_handled_product_ids[] = $product->get_id(); /** * Filters the CSS classes applied to the main single product wrapper. * * This hook lets Blocksy and third-party integrations add classes that * are specific to the currently queried single product. * * Fired at most once per product per request, in these contexts: * - on single product pages (and pages treated as such, e.g. with the * `[product_page]` shortcode) — for the main product only, never for * the related products/upsells loops rendered on the same page; * - during AJAX requests that render single product markup (e.g. the * quick view modal). Listeners whose classes only make sense on the * actual single product page need to narrow themselves down (see * `blocksy_woo_single_post_class()`). * * It is NOT fired on archive/shop loops. * * @since 2.0.72 * * @param string[] $classes List of CSS classes applied to the product wrapper. */ return apply_filters( 'blocksy:woocommerce:single-product:post-class', $classes ); } // Whether the given product's single add-to-cart submits via AJAX. The // add-to-cart layer lookup is memoized on the instance so repeat callers // (theme cart actions, companion floating bar) don't re-parse the layout. public function has_ajax_add_to_cart() { if ($this->has_ajax_add_to_cart_option === null) { $layer = blocksy_get_product_specific_layer('product_add_to_cart'); $this->has_ajax_add_to_cart_option = ( $layer && blocksy_akg('has_ajax_add_to_cart', $layer, 'yes') === 'yes' ); } if (! $this->has_ajax_add_to_cart_option) { return false; } global $product; return ( $product instanceof \WC_Product && ! $product->is_type('external') && get_option('woocommerce_cart_redirect_after_add', 'no') === 'no' ); } public function receive_initial_woo_behavior($initial_woo_behavior) { $this->initial_woo_behavior = array_merge( $this->initial_woo_behavior, $initial_woo_behavior ); } // TODO: skip if no translation plugin is present public function register_translations() { $default_product_layout = blocksy_get_woo_single_layout_defaults(); $layout = blocksy_get_theme_mod( 'woo_single_layout', $default_product_layout ); $layout = blocksy_normalize_layout( $layout, $default_product_layout ); $product_view_type = blocksy_get_product_view_type(); if ( $product_view_type === 'top-gallery' || $product_view_type === 'columns-top-gallery' ) { $woo_single_split_layout = blocksy_get_theme_mod( 'woo_single_split_layout', [ 'left' => blocksy_get_woo_single_layout_defaults('left'), 'right' => blocksy_get_woo_single_layout_defaults('right') ] ); $layout = array_merge( $woo_single_split_layout['left'], $woo_single_split_layout['right'] ); } $additional_info_layer = array_values(array_filter($layout, function($k) { return $k['id'] === 'additional_info'; })); if (empty($additional_info_layer)) { return; } $items = blocksy_akg('additional_info_items', $additional_info_layer[0], []); add_filter( 'blocksy:translations-manager:all-translation-keys', function ($all_keys) use ($additional_info_layer) { $items = blocksy_akg('additional_info_items', $additional_info_layer[0], []); foreach ($items as $key => $item) { $item_i18n_id_prefix = 'single_product:additional_info_item:' . $item['id']; if (isset($item['__id'])) { $item_i18n_id_prefix = 'single_product:additional_info_item:' . $item['__id']; } $all_keys[] = [ 'key' => $item_i18n_id_prefix . ':item_title', 'value' => $item['item_title'] ]; } return $all_keys; } ); } public function render_layout($args = []) { $args = wp_parse_args( $args, [ 'layout' => null, 'defaults' => null, 'exclude' => null ] ); if (! $args['layout']) { $args['layout'] = blocksy_get_theme_mod( 'woo_single_layout', blocksy_get_woo_single_layout_defaults() ); } if ($args['defaults']) { $args['layout'] = blocksy_normalize_layout( $args['layout'], $args['defaults'] ); } /** * Filters the resolved single product layout layers. * * @since 2.0.8 * * @param array $layout List of single product layout layers. */ $args['layout'] = apply_filters( 'blocksy:woocommerce:product-single:layout', $args['layout'] ); /** * Fires before the single product layout layers are rendered. * * @since 2.0.62 * * @param array $args Single product layout arguments. */ do_action('blocksy:woocommerce:product-single:layout:before', $args); foreach ($args['layout'] as $layer) { if (! $layer['enabled']) { continue; } // For now respect initial behavior only for add to cart layer. if ( $layer['id'] === 'product_add_to_cart' && ! $this->initial_woo_behavior['product_add_to_cart'] ) { continue; } if (method_exists($this, $layer['id'])) { $this->{$layer['id']}($layer); continue; } /** * Fires to render a custom single product layout layer that has no * built-in renderer method. * * @since 2.0.1 * * @param array $layer Single product layout layer descriptor. */ do_action('blocksy:woocommerce:product:custom:layer', $layer); } /** * Fires after all single product layout layers are rendered. * * @since 2.0.62 * * @param array $args Single product layout arguments. */ do_action('blocksy:woocommerce:product-single:layout:after', $args); } public function product_title($layer) { /** * Fires before the single product title layer. * * @since 2.0.1 */ do_action('blocksy:woocommerce:product-single:title:before'); woocommerce_template_single_title(); /** * Fires after the single product title layer. * * @since 2.0.1 */ do_action('blocksy:woocommerce:product-single:title:after'); } public function product_rating($layer) { /** * Fires before the single product rating layer. * * @since 2.0.1 */ do_action('blocksy:woocommerce:product-single:rating:before'); woocommerce_template_single_rating(); /** * Fires after the single product rating layer. * * @since 2.0.1 */ do_action('blocksy:woocommerce:product-single:rating:after'); } public function product_price($layer) { /** * Fires before the single product price layer. * * @since 1.8.0 */ do_action('blocksy:woocommerce:product-single:price:before'); woocommerce_template_single_price(); /** * Fires after the single product price layer. * * @since 1.8.0 */ do_action('blocksy:woocommerce:product-single:price:after'); } public function product_desc($layer) { /** * Fires before the single product short description layer. * * @since 1.8.0 */ do_action('blocksy:woocommerce:product-single:excerpt:before'); woocommerce_template_single_excerpt(); /** * Fires after the single product short description layer. * * @since 1.8.4 */ do_action('blocksy:woocommerce:product-single:excerpt:after'); } public function divider($layer) { echo blocksy_html_tag( 'span', [ 'class' => 'ct-product-divider', 'data-id' => blocksy_akg('__id', $layer, 'default') ], '' ); } public function product_add_to_cart($layer) { /** * Fires before the single product add to cart layer. * * @since 2.0.1 */ do_action('blocksy:woocommerce:product-single:add_to_cart:before'); $section_title = blocksy_akg('add_to_cart_layer_title', $layer, ''); ob_start(); woocommerce_template_single_add_to_cart(); $single_add_to_cart = ob_get_clean(); echo blocksy_html_tag( 'div', [ 'class' => 'ct-product-add-to-cart', ], ( ! empty($section_title) || is_customize_preview() ? blocksy_html_tag( 'span', [ 'class' => 'ct-module-title', ], $section_title ) : '' ) . $single_add_to_cart ); /** * Fires after the single product add to cart layer. * * @since 2.0.1 */ do_action('blocksy:woocommerce:product-single:add_to_cart:after'); } public function product_meta($layer) { /** * Fires before the single product meta layer. * * @since 2.0.1 */ do_action('blocksy:woocommerce:product-single:meta:before'); woocommerce_template_single_meta(); /** * Fires after the single product meta layer. * * @since 2.0.1 */ do_action('blocksy:woocommerce:product-single:meta:after'); } public function product_payment_methods($layer) { $items = [ [ 'id' => 'item_mastercard', 'enabled' => true, 'label' => 'Mastercard' ], [ 'id' => 'item_visa', 'enabled' => true, 'label' => 'Visa' ], [ 'id' => 'item_amex', 'enabled' => true, 'label' => 'Amex' ], [ 'id' => 'item_discover', 'enabled' => true, 'label' => 'Discover' ], ]; if (isset($layer['payment_items'])) { $items = $layer['payment_items']; } if (! count($items)) { return; } $data_color = blocksy_akg('payment_icons_color', $layer, 'default'); $section_title = blocksy_translate_dynamic( blocksy_akg( 'payment_methods_title', $layer, __('Guaranteed Safe Checkout', 'blocksy') ), 'single_product:payment_methods:title' ); $out = '
'; echo $out; } public function additional_info($layer) { $items = [ [ 'id' => 'additional_info_item_premium_quality', 'enabled' => true, 'item_title' => __('Premium Quality', 'blocksy'), ], [ 'id' => 'additional_info_item_secure_payments', 'enabled' => true, 'item_title' => __('Secure Payments', 'blocksy'), ], [ 'id' => 'additional_info_item_satisfaction_guarantee', 'enabled' => true, 'item_title' => __('Satisfaction Guarantee', 'blocksy') ], [ 'id' => 'additional_info_item_worldwide_shipping', 'enabled' => true, 'item_title' => __('Worldwide Shipping', 'blocksy') ], [ 'id' => 'additional_info_item_money_back_guarantee', 'enabled' => true, 'item_title' => __('Money Back Guarantee', 'blocksy') ], ]; if (isset($layer['additional_info_items'])) { $items = $layer['additional_info_items']; } if (! count($items)) { return; } $section_title = blocksy_translate_dynamic( blocksy_akg( 'product_additional_info_title', $layer, __('Extra Features', 'blocksy') ), 'single_product:additional_info:title' ); $out = '