* @since 1.0.11 */ class AdvancedProductGrid extends AdvancedPostGrid { private $_query; use Product_Trait; public function get_name() { return 'uicore-advanced-product-grid'; } public function get_categories() { return ['uicore', 'uicore-woo']; } public function get_title() { return __('Advanced Product Grid', 'uicore-elements'); } public function get_icon() { return 'eicon-gallery-grid ui-e-widget'; } public function get_keywords() { return ['woocommerce', 'product', 'shop', 'store', 'post', 'grid']; } public function get_styles() { // get parent get_styles $styles = parent::get_styles(); // remove parent AdvancedPostGrid widget main style. OBS: the IDE complains because get_styles doc // says it returns obj, but we're extending a widget that returns the array, so the parent // is not widget base, but the AdvancedPostGrid widget if (($key = array_search('advanced-post-grid', $styles)) !== false) { unset($styles[$key]); } // Add the product grid style $styles[] = 'advanced-product-grid'; return $styles; } /** * Makes sure some of our custom controls are present. This should be a * temporary fix to a problem that do not happens consistently. */ function custom_controls_verification() { // In some contexts, such as theme-builder, our custom controls are not available // and requiring in register_controls method breaks the widget render in editor if (! defined('DOING_AJAX') || ! DOING_AJAX) { if (! class_exists('UiCoreElements\Controls\Post_Filter')) { require_once UICORE_ELEMENTS_INCLUDES . '/controls/class-post-filter-control.php'; } if (! class_exists('UiCoreElements\Controls\Product_Filter')) { require_once UICORE_ELEMENTS_INCLUDES . '/controls/class-product-filter-control.php'; } } } function get_query() { return $this->_query; } protected function register_controls() { $this->custom_controls_verification(); // Fallback if (!class_exists('WooCommerce')) { $this->start_controls_section( 'section_fallback', [ 'label' => esc_html__('Content', 'uicore-elements'), ] ); $this->add_control( 'woocommerce_warning', [ 'type' => \Elementor\Controls_Manager::ALERT, 'alert_type' => 'warning', 'content' => esc_html__('Please enable WooCommerce to use this widget.', 'uicore-elements'), ] ); $this->end_controls_section(); return; } parent::register_controls(); // Remove query source and meta controls from parent $this->remove_control('posts-filter_post_type'); $this->remove_control('top_meta'); $this->remove_control('before_title_meta'); $this->remove_control('after_title_meta'); $this->remove_control('bottom_meta'); // Register query control with updated options $this->start_injection([ 'of' => 'section_post_grid_def', 'at' => 'after', ]); $this->add_group_control( Product_Filter::get_type(), [ 'name' => 'product-filter', 'label' => esc_html__('Products', 'uicore-elements'), 'description' => esc_html__('Current Query Settings > Reading', 'uicore-elements'), ] ); $this->end_injection(); // Register meta controls with updated options $this->start_injection([ 'of' => 'section_extra_item_content', 'at' => 'after', ]); $this->TRAIT_register_post_meta_controls(false, true); $this->end_injection(); // Register new button placement control $this->start_injection([ 'of' => 'section_button_style', 'at' => 'after', ]); $this->add_control( 'button_position', [ 'label' => esc_html__('Placement', 'uicore-elements'), 'type' => \Elementor\Controls_Manager::SELECT, 'options' => [ '' => 'Default', 'ui-e-button-placement-image' => 'On Image', ], 'default' => 'ui-e-button-placement-image', 'render_type' => 'template', 'prefix_class' => '' ] ); $this->end_injection(); // Register new Swatches and Sale Badge controls $this->start_injection([ 'of' => 'show_button', 'at' => 'after', ]); if (defined('UICORE_ASSETS')) { $this->add_control( 'show_swatches', [ 'label' => esc_html__('Swatches', 'uicore-elements'), 'type' => \Elementor\Controls_Manager::SWITCHER, 'label_on' => esc_html__('Show', 'uicore-elements'), 'label_off' => esc_html__('Hide', 'uicore-elements'), 'description' => esc_html__('Will only work if you have Uicore Framework plugin active at least on version 6.0.1', 'uicore-elements'), 'default' => 'yes', ] ); } $this->add_control( 'show_sale_badge', [ 'label' => esc_html__('Sale Badge', 'uicore-elements'), 'type' => \Elementor\Controls_Manager::SWITCHER, 'label_on' => esc_html__('Show', 'uicore-elements'), 'label_off' => esc_html__('Hide', 'uicore-elements'), 'default' => 'yes', 'prefix_class' => 'ui-e-show-sale-badge-', ] ); $this->add_control( 'badge_warning', [ 'type' => \Elementor\Controls_Manager::RAW_HTML, 'raw' => __("If you can't see the Sale Badge after setting 'Show', you problably didn't set any Meta option as 'Product Sale', or you don't have products on sale.", 'uicore-elements'), 'content_classes' => 'elementor-control-field-description', 'condition' => [ 'show_sale_badge' => 'yes', ], ] ); $this->end_injection(); if (defined('UICORE_ASSETS')) { $this->start_injection([ 'of' => 'section_style_content', 'at' => 'after', ]); $this->start_controls_section( 'section_swatches', [ 'label' => esc_html__('Swatches', 'uicore-elements') . UICORE_ELEMENTS_NEW_OPTION, 'tab' => \Elementor\Controls_Manager::TAB_STYLE, 'condition' => [ 'show_swatches' => 'yes', ], ] ); $this->add_control( 'swatches_to', [ 'label' => esc_html__('Use Framework defaults', 'uicore-elements'), 'type' => \Elementor\Controls_Manager::SWITCHER, 'description' => esc_html__('If enabled, the swatches will inherit your theme options.', 'uicore-elements'), 'label_on' => esc_html__('Yes', 'uicore-elements'), 'label_off' => esc_html__('No', 'uicore-elements'), 'default' => 'yes', ] ); $this->add_control( 'swatches_size', [ 'label' => esc_html__('Size', 'uicore-elements'), 'type' => \Elementor\Controls_Manager::SLIDER, 'size_units' => ['px'], 'range' => [ 'px' => [ 'min' => 10, 'max' => 100, ], ], 'default' => [ 'unit' => 'px', 'size' => 30, ], 'condition' => [ 'show_swatches' => 'yes', 'swatches_to!' => 'yes', ], 'selectors' => [ '{{WRAPPER}}' => '--uicore-swatch-size: {{SIZE}}{{UNIT}};', ], ] ); $this->add_control( 'swatches_outer_radius', [ 'label' => esc_html__('Outer Border Radius', 'uicore-elements'), 'type' => \Elementor\Controls_Manager::SLIDER, 'size_units' => ['%', 'px'], 'range' => [ '%' => [ 'min' => 0, 'max' => 100, ], 'px' => [ 'min' => 0, 'max' => 50, ], ], 'default' => [ 'unit' => '%', 'size' => 50, ], 'separator' => 'before', 'condition' => [ 'show_swatches' => 'yes', 'swatches_to!' => 'yes', ], 'selectors' => [ '{{WRAPPER}}' => '--uicore-swatch-radius: {{SIZE}}{{UNIT}}; --uicore-swatch-big-radius: {{SIZE}}{{UNIT}};', '{{WRAPPER}} .uicore-swatch.uicore-swatch--color' => 'border-radius: {{SIZE}}{{UNIT}};', ], ] ); $this->add_control( 'swatches_inner_radius', [ 'label' => esc_html__('Inner Border Radius', 'uicore-elements'), 'type' => \Elementor\Controls_Manager::SLIDER, 'size_units' => ['px', '%'], 'range' => [ 'px' => [ 'min' => 0, 'max' => 100, ], '%' => [ 'min' => 0, 'max' => 50, ], ], 'default' => [ 'unit' => 'px', 'size' => 4, ], 'separator' => 'after', 'condition' => [ 'show_swatches' => 'yes', 'swatches_to!' => 'yes', ], 'selectors' => [ '{{WRAPPER}} .uicore-swatch > div' => 'border-radius: {{SIZE}}{{UNIT}};', ], ] ); $this->add_group_control( \Elementor\Group_Control_Border::get_type(), [ 'name' => 'swatch_border', 'selector' => '{{WRAPPER}} .uicore-swatch', 'condition' => [ 'show_swatches' => 'yes', 'swatches_to!' => 'yes', ], ] ); $this->add_control( 'hover_border_size', [ 'label' => esc_html__('Hover Border Size', 'uicore-elements'), 'type' => \Elementor\Controls_Manager::SLIDER, 'size_units' => ['px'], 'range' => [ 'px' => [ 'min' => 0, 'max' => 5, ], ], 'default' => [ 'unit' => 'px', 'size' => 1, ], 'condition' => [ 'show_swatches' => 'yes', 'swatches_to!' => 'yes', ], 'selectors' => [ '{{WRAPPER}} .uicore-swatch:hover' => 'box-shadow: 0 0 0 {{SIZE}}{{UNIT}} var(--uicore-swatch-border);', ], ] ); $this->add_control( 'swatch_hover_color', [ 'label' => esc_html__('Hover Border Color', 'uicore-elements'), 'type' => \Elementor\Controls_Manager::COLOR, 'condition' => [ 'show_swatches' => 'yes', 'swatches_to!' => 'yes', ], 'selectors' => [ '{{WRAPPER}}' => '--uicore-swatch-border: {{VALUE}};', '{{WRAPPER}} .uicore-swatch:hover' => 'border-color: {{VALUE}};', ], ] ); $this->end_controls_section(); $this->end_injection(); } // Register new Hide out of stock products control $this->start_injection([ 'of' => 'sticky', 'at' => 'before', ]); $this->add_control( 'hide_out_of_stock', [ 'label' => esc_html__('Hide out of stock', 'uicore-elements'), 'type' => \Elementor\Controls_Manager::SWITCHER, 'condition' => [ 'product-filter_post_type!' => 'related', ], ] ); $this->end_injection(); $this->start_injection([ 'of' => 'text', 'at' => 'after', ]); $this->add_control( 'variations_text', [ 'type' => \Elementor\Controls_Manager::TEXT, 'label' => esc_html__('Select Options Text', 'uicore-elements'), 'label_block' => true, 'default' => '', 'description' => esc_html__('Variable product purchase text.', 'uicore-elements'), 'placeholder' => esc_html__('Select Options', 'uicore-elements'), ] ); $this->add_control( 'unavailable_text', [ 'type' => \Elementor\Controls_Manager::TEXT, 'label' => esc_html__('Unavailable Text', 'uicore-elements'), 'label_block' => true, 'default' => '', 'description' => esc_html__('Out of stock or not available purchase text.', 'uicore-elements'), 'placeholder' => esc_html__('Read more', 'uicore-elements'), ] ); $this->end_injection(); // Update controls that uses 'posts_filter' to 'product_filter' $this->update_control( 'item_limit', [ 'condition' => [ 'product-filter_post_type!' => 'current', ], ] ); // Update filters options $this->update_control( 'filters_taxonomies', [ 'options' => Helper::get_taxonomies(true, true), 'default' => ['product_cat'], ] ); // Update Image animation options and default value $this->update_control( 'anim_image', [ 'options' => [ '' => __('None', 'uicore-elements'), 'ui-e-img-anim-zoom' => __('Zoom', 'uicore-elements'), 'ui-e-img-anim-change' => __('Change image', 'uicore-elements'), ], 'default' => 'ui-e-img-anim-change', 'render_type' => 'template', // `change image` adds a new html element ] ); // Update top meta style options so sale badge (default top meta) looks better by default $this->update_control( 'top_meta_color', [ 'default' => '#FFF', ] ); $this->update_control( 'top_meta_background', [ 'default' => '#000', ] ); $this->update_control( 'top_meta_padding', [ 'default' => [ 'top' => 5, 'right' => 5, 'bottom' => 5, 'left' => 5, 'unit' => 'px' ], ] ); // Update some UI controls labels and default values $this->update_control( 'excerpt', [ 'label' => esc_html__('Product Summary', 'uicore-elements'), 'default' => '' ] ); $this->update_control( 'excerpt_trim', ['label' => esc_html__('Summary Length (words)', 'uicore-elements')] ); $this->update_control( 'show_button', [ 'label' => esc_html__('Add to Cart Button', 'uicore-elements'), 'default' => 'yes' ] ); $this->update_control( 'text', [ 'label' => esc_html__('Add to Cart Text', 'uicore-elements'), 'label_block' => true, 'default' => '', 'placeholder' => esc_html__('Add to cart', 'uicore-elements'), ] ); $this->update_control( 'text_color', ['label' => esc_html__('Summary Color', 'uicore-elements')] ); $this->update_control( 'text_typography', ['label' => esc_html__('Summary Typography', 'uicore-elements')] ); $this->update_control( 'text_gap', ['label' => esc_html__('Summary Top Space', 'uicore-elements')] ); // Update button controls $this->update_control( 'border_radius', // button border-radius [ 'default' => ['top' => 0, 'right' => 0, 'bottom' => 0, 'left' => 0] ] ); $this->update_control( 'text_padding', // button padding [ 'default' => ['top' => 10, 'right' => 10, 'bottom' => 10, 'left' => 10, 'unit' => 'px'] ] ); $this->update_control( 'button_align', [ 'condition' => [ 'button_position!' => 'ui-e-button-placement-image', ], // since product might have ajax add to cart, wich has flex display so the cart added icon do not break button layout, // we justify the flex content to center so justified text align button keeps text centralized. See: https://uicore.atlassian.net/browse/ELM-429 'selectors' => [ '{{WRAPPER}} .ui-e-readmore' => 'align-self: {{VALUE}}; justify-content: center;', ] ] ); $this->update_control( 'button_gap', [ 'condition' => [ 'button_position!' => 'ui-e-button-placement-image', ] ] ); } /** * Renders the widget content using AJAX. * * This method retrieves the widget settings; set up the query, and renders each post item. * If no posts are found, it returns false. After rendering, it resets the query and returns the output. * * @param array|false $current_query The current query args, or false if the widget isn't set to use the current query. * @param int $pageID The ID of the page where the widget is located. * * @return string|false The rendered widget content or false if no posts are found. */ public function render_ajax($current_query) { // Get settings and post type $settings = $this->get_settings(); $this->TRAIT_query_products($settings, $current_query); $wc_query = $this->get_query(); $products = wc_get_products($wc_query); // No products found if (empty($products)) { return false; } \ob_start(); foreach ($products as $index => $product) { $post_object = get_post($product->get_id()); setup_postdata($post_object); $this->TRAIT_render_product($product, $index, false, true, true); wp_reset_postdata(); } $markup = \ob_get_clean(); return [ 'markup' => $markup, 'total_pages' => $wc_query['total_pages'] ]; } protected function render() { if (!class_exists('WooCommerce')) { if ($this->is_edit_mode()) { echo '
' . esc_html__('Please enable WooCommerce to use this widget.', 'uicore-elements') . '
'; } return; } global $wp_query; $settings = $this->get_settings(); // Get product ID $product_id = get_the_ID(); // Related products, on save page action at editor, triggers an fata error upon post object handle. // Prevent it for now by disabling the widget on edit mode. TODO: remove it after fixing the issue if ('related' === $settings['product-filter_post_type'] && $this->is_edit_mode()) { echo '' . esc_html__('This widget is set to display related products. To see the results, please preview the page, or customize your widget before setting "current" query.', 'uicore-elements') . '
'; return; } // Build query $products = $this->TRAIT_query_products($settings, $wp_query->query); $wc_query = $this->get_query(); // Store widget settings in a transient $ID = strval($this->get_ID()); set_transient('ui_elements_widgetdata_' . $ID, $settings, \MONTH_IN_SECONDS); // Get the quantity of items and creates a loop control $items = $settings['item_limit']['size']; $loops = 0; $this->TRAIT_render_filters($settings, true); // No posts found if (empty($products)) { echo '' . esc_html__('No products found.', 'uicore-elements') . '
'; } else { ?>