PluginProbe
ShopBuilder – WooCommerce Builder For Elementor / 3.4.0
ShopBuilder – WooCommerce Builder For Elementor v3.4.0
3.4.2 3.4.1 3.4.0 2.0.1 2.0.2 2.0.3 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 All 63 releases
← All changes | app/Elementor/Helper/RenderHelpers.php +1162 -49 2.1.73.4.0 View file →
@@ -8,10 +8,13 @@
8 8 */
9 9
10 10 namespace RadiusTheme\SB\Elementor\Helper;
11 11
12 -use RadiusTheme\SB\Helpers\BuilderFns;
12 +use Elementor\Plugin;
13 +use RadiusTheme\SB\Helpers\Cache;
13 14 use RadiusTheme\SB\Helpers\Fns;
15 +use WC_Product_Query;
16 +use WP_Term;
14 17
15 18 // Do not allow directly accessing this file.
16 19 if ( ! defined( 'ABSPATH' ) ) {
17 20 exit( 'This script cannot be accessed directly.' );
@@ -21,8 +24,33 @@
21 24 * BuilderFns class
22 25 */
23 26 class RenderHelpers {
24 27 /**
28 + * @var array
29 + */
30 + private static $cache = [];
31 + /**
32 + * @param string $order_by value.
33 + * @return string
34 + */
35 + public static function escaping_product_orderby( $order_by ) {
36 + $catalog_orderby_options = apply_filters(
37 + 'woocommerce_catalog_orderby',
38 + [
39 + 'menu_order' => __( 'Default sorting', 'shopbuilder' ),
40 + 'popularity' => __( 'Sort by popularity', 'shopbuilder' ),
41 + 'rating' => __( 'Sort by average rating', 'shopbuilder' ),
42 + 'date' => __( 'Sort by latest', 'shopbuilder' ),
43 + 'price' => __( 'Sort by price: low to high', 'shopbuilder' ),
44 + 'price-desc' => __( 'Sort by price: high to low', 'shopbuilder' ),
45 + ]
46 + );
47 + if ( in_array( $order_by, array_keys( $catalog_orderby_options ), true ) ) {
48 + return $order_by;
49 + }
50 + return 'menu_order';
51 + }
52 + /**
25 53 * Get data.
26 54 *
27 55 * @param array|string $haystack The array or string to search for the value.
28 56 * @param string $needle The key to look for in the array or the string itself.
@@ -83,8 +111,9 @@
83 111 'order' => self::get_data( $meta, 'posts_order', 'DESC' ),
84 112 'author_in' => self::get_data( $meta, 'filter_author', [] ),
85 113 'display_by' => self::get_data( $meta, 'products_filter', 'date' ),
86 114 'categories' => self::get_data( $meta, 'filter_categories', [] ),
115 + 'brands' => self::get_data( $meta, 'filter_brands', [] ),
87 116 'tags' => self::get_data( $meta, 'filter_tags', [] ),
88 117 'attributes' => self::get_data( $meta, 'filter_attributes', [] ),
89 118 'relation' => self::get_data( $meta, 'tax_relation', 'OR' ),
90 119 'category_source' => self::get_data( $meta, 'select_source', 'product_cat' ),
@@ -107,9 +136,9 @@
107 136
108 137 // Pagination.
109 138 'pagination' => ! empty( $meta['show_pagination'] ),
110 139 'posts_loading_type' => self::get_data( $meta, 'pagination_type', 'pagination' ),
111 - 'posts_per_page' => self::get_data( $meta, 'pagination_per_page', '' ),
140 + 'posts_per_page' => self::get_data( $meta, 'pagination_per_page', '' ) ?: ( ! empty( $meta['posts_per_page'] ) ? absint( $meta['posts_per_page'] ) : '' ),
112 141
113 142 // Image.
114 143 'f_img' => ! empty( $meta['show_featured_image'] ),
115 144 'gallery_img' => ! empty( $meta['show_product_gallery'] ),
@@ -196,8 +225,9 @@
196 225 'auto_play' => ! empty( $meta['slide_autoplay'] ),
197 226 'stop_on_hover' => ! empty( $meta['pause_hover'] ),
198 227 'slider_nav' => ! empty( $meta['slider_nav'] ),
199 228 'slider_dot' => ! empty( $meta['slider_pagi'] ),
229 + 'slider_dynamic_dot' => ! empty( $meta['slider_dynamic_pagi'] ),
200 230 'loop' => ! empty( $meta['slider_loop'] ),
201 231 'lazy_load' => ! empty( $meta['slider_lazy_load'] ),
202 232 'auto_height' => ! empty( $meta['slider_auto_height'] ),
203 233 'speed' => self::get_data( $meta, 'slide_speed', 2000 ),
@@ -232,11 +262,13 @@
232 262
233 263 $queried_obj = get_queried_object();
234 264
235 265 if ( is_shop() || is_product_taxonomy() ) {
236 - $data['posts_per_page'] = self::get_products_per_page();
237 - $data['order_by'] = self::get_data( $data, 'rtsb_orderby', $data['order_by'] );
238 - $data['order'] = self::get_data( $data, 'rtsb_order', $data['order'] );
266 + if ( empty( $data['posts_per_page'] ) ) {
267 + $data['posts_per_page'] = self::get_products_per_page();
268 + }
269 + $data['order_by'] = self::get_data( $data, 'rtsb_orderby', $data['order_by'] );
270 + $data['order'] = self::get_data( $data, 'rtsb_order', $data['order'] );
239 271 }
240 272
241 273 if ( ! is_shop() && is_product_taxonomy() ) {
242 274 if ( ! empty( $queried_obj->taxonomy ) ) {
@@ -247,8 +279,16 @@
247 279 $data['queried_term'] = esc_html( $queried_obj->term_id );
248 280 }
249 281 }
250 282
283 + if ( is_search() ) {
284 + $search_query = get_search_query();
285 +
286 + if ( $search_query ) {
287 + $data['search_query'] = sanitize_text_field( $search_query );
288 + }
289 + }
290 +
251 291 return apply_filters( 'rtsb/elementor/render/meta_dataset_final', $data, $meta, $raw_settings );
252 292 }
253 293
254 294 /**
@@ -272,8 +312,12 @@
272 312 'comparison_button' => ! empty( $meta['comparison_button'] ),
273 313 'quick_view_button' => ! empty( $meta['quick_view_button'] ),
274 314 'show_rating' => ! empty( $meta['show_rating'] ),
275 315 'show_pagination' => ! empty( $meta['show_pagination'] ),
316 + // The base Product Archive widget has no pagination-type control: with filters it
317 + // uses Ajax Load More (see the widget's pagination notice). Custom-layout widgets
318 + // pass their own pagination_type and use a different dataset.
319 + 'posts_loading_type' => self::get_data( $meta, 'pagination_type', 'load_more' ),
276 320 'cart_icon' => self::get_data( $meta, 'cart_icon', [] ),
277 321 'wishlist_icon' => self::get_data( $meta, 'wishlist_icon', [] ),
278 322 'wishlist_icon_added' => self::get_data( $meta, 'wishlist_icon_added', [] ),
279 323 'comparison_icon' => self::get_data( $meta, 'comparison_icon', [] ),
@@ -290,9 +334,13 @@
290 334 );
291 335
292 336 $queried_obj = get_queried_object();
293 337
294 - if ( ! is_shop() && BuilderFns::is_archive() ) {
338 + // Use WooCommerce's own taxonomy check (covers product_cat, product_tag
339 + // and product_brand) so the queried term is recorded on every product
340 + // taxonomy archive — Ajax load-more / filters scope to it. Mirrors the
341 + // custom-layout dataset in self::meta_dataset().
342 + if ( ! is_shop() && is_product_taxonomy() ) {
295 343 if ( ! empty( $queried_obj->taxonomy ) ) {
296 344 $data['queried_tax'] = esc_html( $queried_obj->taxonomy );
297 345 }
298 346
@@ -300,8 +348,16 @@
300 348 $data['queried_term'] = esc_html( $queried_obj->term_id );
301 349 }
302 350 }
303 351
352 + if ( is_search() ) {
353 + $search_query = get_search_query();
354 +
355 + if ( $search_query ) {
356 + $data['search_query'] = sanitize_text_field( $search_query );
357 + }
358 + }
359 +
304 360 return apply_filters( 'rtsb/elementor/render/archive_meta_dataset_final', $data, $meta );
305 361 }
306 362
307 363 /**
@@ -340,9 +396,16 @@
340 396
341 397 if ( ! empty( $settings['single_category'] ) ) {
342 398 $visibility[] = 'single-cat';
343 399 }
400 + if ( ! empty( $settings['show_brands'] ) ) {
401 + $visibility[] = 'brands';
402 + }
344 403
404 + if ( ! empty( $settings['single_brand'] ) ) {
405 + $visibility[] = 'single-brand';
406 + }
407 +
345 408 if ( ! empty( $settings['show_swatches'] ) ) {
346 409 $visibility[] = 'swatches';
347 410 }
348 411
@@ -408,13 +471,23 @@
408 471 * @return int
409 472 */
410 473 public static function default_columns( $layout ) {
411 474 switch ( $layout ) {
475 + case 'rtsb-post-grid-layout1':
476 + case 'rtsb-post-grid-layout2':
477 + case 'rtsb-post-grid-layout3':
478 + case 'rtsb-team-layout1':
479 + case 'rtsb-team-layout2':
480 + case 'rtsb-testimonial-layout2':
412 481 case 'grid-layout2':
413 482 case 'slider-layout2':
483 + case 'rtsb-coupon-layout1':
484 + case 'rtsb-coupon-layout2':
485 + case 'rtsb-coupon-layout3':
414 486 $columns = 3;
415 487 break;
416 -
488 + case 'rtsb-post-list-layout1':
489 + case 'rtsb-post-list-layout2':
417 490 case 'list-layout1':
418 491 case 'list-layout2':
419 492 case 'list-layout3':
420 493 case 'list-layout7':
@@ -420,9 +493,13 @@
420 493 case 'list-layout7':
421 494 case 'list-layout5':
422 495 $columns = 1;
423 496 break;
424 -
497 + case 'rtsb-team-layout4':
498 + case 'rtsb-testimonial-layout1':
499 + case 'rtsb-testimonial-layout3':
500 + case 'rtsb-testimonial-layout4':
501 + case 'rtsb-testimonial-layout5':
425 502 case 'list-layout4':
426 503 case 'list-layout6':
427 504 case 'slider-layout5':
428 505 case 'slider-layout8':
@@ -427,10 +504,12 @@
427 504 case 'slider-layout5':
428 505 case 'slider-layout8':
429 506 $columns = 2;
430 507 break;
431 -
508 + case 'rtsb-logo-layout1':
509 + case 'rtsb-logo-layout2':
432 510 case 'grid-layout6':
511 + case 'grid-layout9':
433 512 $columns = 5;
434 513 break;
435 514
436 515 case 'category-layout1':
@@ -471,9 +550,10 @@
471 550 public static function prepare_container_classes( $metas = [], $settings = [], $custom_class = '' ) {
472 551 $classes = 'rtsb-elementor-container products rtsb-pos-r ';
473 552 $classes .= $custom_class;
474 553
475 - $layout = $metas['layout'];
554 + $raw_layout = esc_html( $metas['layout'] );
555 + $layout = self::filter_layout( $raw_layout );
476 556 $is_cat = preg_match( '/category/', $layout );
477 557 $cart_txt_class = '';
478 558
479 559 $animation = $metas['hover_animation'];
@@ -478,9 +558,9 @@
478 558
479 559 $animation = $metas['hover_animation'];
480 560
481 561 if ( ! $is_cat ) {
482 - $cart_txt_class = $metas['show_cart_text'] ? ' has-cart-text' : ' no-cart-text';
562 + $cart_txt_class = $metas['show_cart_text'] ? ' show-cart-text' : ' no-cart-text';
483 563 $animation .= ' gallery-hover-' . ( ! empty( $settings['gallery_hover_animation'] ) ? $settings['gallery_hover_animation'] : 'fade' );
484 564 }
485 565
486 566 $badge_class = [
@@ -501,14 +581,17 @@
501 581
502 582 if ( in_array( 'single-cat', $metas['visibility'], true ) ) {
503 583 $classes .= ' show-single-cat';
504 584 }
585 + if ( in_array( 'single-brand', $metas['visibility'], true ) ) {
586 + $classes .= ' show-single-brand';
587 + }
505 588
506 589 if ( ! $is_cat ) {
507 590 $action_btn_classes = [
508 - 'show_compare_text' => 'has-compare-text',
509 - 'show_quick_view_text' => 'has-quick-view-text',
510 - 'show_wishlist_text' => 'has-wishlist-text',
591 + 'show_compare_text' => 'show-compare-text',
592 + 'show_quick_view_text' => 'show-quick-view-text',
593 + 'show_wishlist_text' => 'show-wishlist-text',
511 594 'show_compare_icon' => 'no-compare-icon',
512 595 'show_quick_view_icon' => 'no-quick-view-icon',
513 596 'show_wishlist_icon' => 'no-wishlist-icon',
514 597 'show_quick_checkout_icon' => 'no-quick_checkout-icon',
@@ -533,8 +616,36 @@
533 616 return apply_filters( 'rtsb/product/container/class', $classes, $settings );
534 617 }
535 618
536 619 /**
620 + * Function to filter and validate the layout against allowed patterns.
621 + *
622 + * @param string $layout The layout string to be filtered.
623 + * @param string $template The template to be checked.
624 + *
625 + * @return string
626 + */
627 + public static function filter_layout( $layout, $template = '' ) {
628 + $allowed_patterns = apply_filters( 'rtsb/elementor/custom_layout', [ 'grid', 'list', 'slider', 'category', 'toyup', 'zilly', 'rtsb' ] );
629 + $layout_part = explode( '-', $layout );
630 + $default = 'grid-layout1';
631 +
632 + if ( empty( $layout_part[0] ) ) {
633 + return $default;
634 + }
635 +
636 + if ( in_array( $layout_part[0], $allowed_patterns, true ) ) {
637 + if ( ! rtsb()->has_pro() && ! in_array( $layout, array_keys( Fns::free_layouts( $layout ) ), true ) ) {
638 + $layout = self::set_default_layout( $layout );
639 + }
640 +
641 + return $layout;
642 + } else {
643 + return ! empty( $template ) ? self::set_default_layout( $layout ) : $default;
644 + }
645 + }
646 +
647 + /**
537 648 * Row classes
538 649 *
539 650 * @param array $settings Settings array.
540 651 *
@@ -542,11 +653,15 @@
542 653 */
543 654 public static function prepare_row_classes( $settings = [] ) {
544 655 $masonry_class = null;
545 656 $classes = 'rtsb-row rtsb-content-loader element-loading';
546 - $loader_class = ' rtsb-pre-loader';
547 657
548 - $layout = $settings['layout'];
658 + $loader_class = '';
659 + if ( Fns::enable_loader() ) {
660 + $loader_class = ' rtsb-pre-loader';
661 + }
662 + $raw_layout = esc_html( $settings['layout'] );
663 + $layout = self::filter_layout( $raw_layout );
549 664 $grid_type = $settings['grid_type'];
550 665 $is_carousel = preg_match( '/slider/', $layout );
551 666
552 667 if ( preg_match( '/category/', $layout ) && ! empty( $settings['active_cat_slider'] ) ) {
@@ -552,23 +667,30 @@
552 667 if ( preg_match( '/category/', $layout ) && ! empty( $settings['active_cat_slider'] ) ) {
553 668 $classes .= ' rtsb-slider-layout';
554 669 }
555 670
556 - if ( 'even' === $grid_type ) {
557 - $masonry_class = ' rtsb-even';
671 + if ( 'equal' === $grid_type ) {
672 + $masonry_class = ' rtsb-equal';
558 673 } elseif ( 'masonry' === $grid_type ) {
559 674 $masonry_class = ' rtsb-masonry';
675 + } else {
676 + $masonry_class = ' rtsb-even';
560 677 }
561 678
562 679 if ( ! rtsb()->has_pro() || $is_carousel ) {
563 680 $masonry_class = ' rtsb-even';
564 681 }
682 +
565 683 if ( $is_carousel && ! empty( $settings['raw_settings']['always_show_nav'] ) ) {
566 684 $classes .= ' always-show-nav';
567 685 }
686 +
568 687 if ( ! empty( $settings['raw_settings']['inner_slider_always_show_nav'] ) ) {
569 688 $classes .= ' inner-slider-always-show-nav';
570 689 }
690 + if ( empty( $settings['raw_settings']['cols_mobile'] ) ) {
691 + $classes .= ' rtsb-mobile-flex-row';
692 + }
571 693
572 694 $classes .= ' rtsb-' . $layout . $masonry_class . $loader_class;
573 695
574 696 return apply_filters( 'rtsb/elementor/row/classes', $classes, $settings );
@@ -576,13 +698,13 @@
576 698
577 699 /**
578 700 * Prepare pagination attributes.
579 701 *
580 - * @param string $layout The layout type.
702 + * @param string $layout The layout type.
581 703 * @param bool $has_filters Whether the widget has filters.
582 - * @param string $template The template name.
583 - * @param array $settings The widget settings.
584 - * @param bool $ajax Whether to enable AJAX pagination.
704 + * @param string $template The template name.
705 + * @param array $settings The widget settings.
706 + * @param bool $ajax Whether to enable AJAX pagination.
585 707 *
586 708 * @return string
587 709 */
588 710 public static function prepare_pagination_attr( $layout, $has_filters, $template, $settings, $ajax ) {
@@ -593,11 +715,12 @@
593 715 return $has_filters ? self::pagination_data( $settings, $template ) : '';
594 716 } elseif ( $is_cat ) {
595 717 return '';
596 718 } else {
597 - $ajax_pagination = ! empty( $settings['show_pagination'] ) && 'pagination' !== $settings['pagination_type'];
598 - $page = Fns::product_filters_has_ajax( 'shop' ) || Fns::product_filters_has_ajax( 'archive' );
599 - $condition = rtsb()->has_pro() && $ajax && ( $ajax_pagination || $has_filters || $page );
719 + $ajax_pagination = ! empty( $settings['show_pagination'] ) && 'pagination' !== $settings['pagination_type'];
720 + $page = Fns::product_filters_has_ajax( apply_filters( 'rtsb/builder/set/current/page/type', '' ) );
721 + $is_shop_or_archive = is_shop() || is_product_taxonomy();
722 + $condition = rtsb()->has_pro() && $ajax && ( $ajax_pagination || $has_filters || $page || $is_shop_or_archive );
600 723
601 724 return $condition ? self::pagination_data( $settings, $template ) : '';
602 725 }
603 726 }
@@ -624,15 +747,16 @@
624 747 *
625 748 * @return array
626 749 */
627 750 public static function slider_data( array $meta, $settings = [] ) {
628 - $has_dots = $meta['slider_dot'] ? ' has-dot' : ' no-dot';
629 - $has_dots .= $meta['slider_nav'] ? ' has-nav' : ' no-nav';
630 - $d_col = 0 === $meta['d_cols'] ? self::default_columns( $meta['layout'] ) : $meta['d_cols'];
631 - $t_col = 0 === $meta['t_cols'] ? 2 : $meta['t_cols'];
632 - $m_col = 0 === $meta['m_cols'] ? 1 : $meta['m_cols'];
751 + $has_dots = $meta['slider_dot'] ? ' has-dot' : ' no-dot';
752 + $has_dots .= $meta['slider_nav'] ? ' has-nav' : ' no-nav';
753 + $has_dynamic_dots = $meta['slider_dynamic_dot'] ? true : false;
754 + $d_col = 0 === $meta['d_cols'] ? self::default_columns( $meta['layout'] ) : $meta['d_cols'];
755 + $t_col = 0 === $meta['t_cols'] ? 2 : $meta['t_cols'];
756 + $m_col = 0 === $meta['m_cols'] ? 1 : $meta['m_cols'];
633 757
634 - if ( \Elementor\Plugin::$instance->breakpoints->has_custom_breakpoints() ) {
758 + if ( Plugin::$instance->breakpoints->has_custom_breakpoints() ) {
635 759 // Widescreen.
636 760 $w_col = self::get_data( $settings, 'cols_widescreen' );
637 761 $w_col = 0 === $w_col ? self::default_columns( $meta['layout'] ) : $w_col;
638 762 $w_group = self::get_data( $settings, 'cols_group_widescreen', 1 );
@@ -663,10 +787,13 @@
663 787 $breakpoints = [
664 788 0 => [
665 789 'slidesPerView' => absint( $m_col ),
666 790 'slidesPerGroup' => absint( $meta['m_group'] ),
791 + 'grid' => [
792 + 'rows' => 1,
793 + ],
667 794 'pagination' => [
668 - 'dynamicBullets' => true,
795 + 'dynamicBullets' => $has_dynamic_dots,
669 796 ],
670 797 ],
671 798 ];
672 799
@@ -691,16 +818,16 @@
691 818 }
692 819
693 820 foreach ( $el_breakpoints as $el_breakpoint => $value ) {
694 821 $breakpoint_value = ! empty( $value['value'] ) ? $value['value'] : $value['default_value'];
695 - $dynamic_bullets = false;
822 + $dynamic_bullets = $has_dynamic_dots;
696 823 $slides_per_view = $d_col;
697 824 $slides_per_group = $meta['d_group'];
698 825
699 826 switch ( $el_breakpoint ) {
700 827 case 'widescreen':
701 - $slides_per_view = $w_col;
702 - $slides_per_group = $w_group;
828 + $slides_per_view = $w_col;
829 + $slides_per_group = $w_group;
703 830
704 831 break;
705 832
706 833 case 'laptop':
@@ -709,10 +836,10 @@
709 836
710 837 break;
711 838
712 839 case 'tablet_extra':
713 - $slides_per_view = $l_col;
714 - $slides_per_group = $l_group;
840 + $slides_per_view = $l_col;
841 + $slides_per_group = $l_group;
715 842
716 843 if ( empty( $el_breakpoints['laptop'] ) ) {
717 844 $slides_per_view = $d_col;
718 845 $slides_per_group = $meta['d_group'];
@@ -737,11 +864,11 @@
737 864
738 865 break;
739 866
740 867 case 'mobile_extra':
741 - $slides_per_view = $t_col;
742 - $slides_per_group = $meta['t_group'];
743 - $dynamic_bullets = true;
868 + $slides_per_view = $t_col;
869 + $slides_per_group = $meta['t_group'];
870 + $dynamic_bullets = $has_dynamic_dots;
744 871
745 872 break;
746 873
747 874 case 'mobile':
@@ -746,9 +873,9 @@
746 873
747 874 case 'mobile':
748 875 $slides_per_view = $t_col;
749 876 $slides_per_group = $meta['t_group'];
750 - $dynamic_bullets = true;
877 + $dynamic_bullets = $has_dynamic_dots;
751 878
752 879 if ( ! empty( $el_breakpoints['mobile_extra'] ) ) {
753 880 $slides_per_view = $me_col;
754 881 $slides_per_group = $me_group;
@@ -774,9 +901,9 @@
774 901 0 => [
775 902 'slidesPerView' => absint( $m_col ),
776 903 'slidesPerGroup' => absint( $meta['m_group'] ),
777 904 'pagination' => [
778 - 'dynamicBullets' => true,
905 + 'dynamicBullets' => $has_dynamic_dots,
779 906 ],
780 907 ],
781 908 768 => [
782 909 'slidesPerView' => absint( $t_col ),
@@ -781,9 +908,9 @@
781 908 768 => [
782 909 'slidesPerView' => absint( $t_col ),
783 910 'slidesPerGroup' => absint( $meta['t_group'] ),
784 911 'pagination' => [
785 - 'dynamicBullets' => false,
912 + 'dynamicBullets' => $has_dynamic_dots,
786 913 ],
787 914 ],
788 915 1025 => [
789 916 'slidesPerView' => absint( $d_col ),
@@ -788,9 +915,9 @@
788 915 1025 => [
789 916 'slidesPerView' => absint( $d_col ),
790 917 'slidesPerGroup' => absint( $meta['d_group'] ),
791 918 'pagination' => [
792 - 'dynamicBullets' => false,
919 + 'dynamicBullets' => $has_dynamic_dots,
793 920 ],
794 921 ],
795 922 ];
796 923 }
@@ -840,9 +967,11 @@
840 967
841 968 if ( $is_carousel ) {
842 969 $loader_class = ' full-op';
843 970 }
844 -
971 + if ( ! Fns::enable_loader() ) {
972 + return '';
973 + }
845 974 return '<div class="rtsb-elements-loading rtsb-ball-clip-rotate"><div></div></div>';
846 975 }
847 976
848 977 /**
@@ -883,10 +1012,11 @@
883 1012 *
884 1013 * @return void
885 1014 */
886 1015 public static function register_scripts( $scripts ) {
887 - $caro = false;
888 - $script = [];
1016 + $caro = false;
1017 + $masonry = false;
1018 + $script = [];
889 1019
890 1020 $script[] = 'jquery';
891 1021
892 1022 foreach ( $scripts as $sc => $value ) {
@@ -893,8 +1023,12 @@
893 1023 if ( ! empty( $sc ) ) {
894 1024 if ( 'isCarousel' === $sc ) {
895 1025 $caro = $value;
896 1026 }
1027 +
1028 + if ( 'isMasonry' === $sc ) {
1029 + $masonry = $value;
1030 + }
897 1031 }
898 1032 }
899 1033
900 1034 if ( count( $scripts ) ) {
@@ -904,11 +1038,15 @@
904 1038 if ( $caro ) {
905 1039 $script[] = 'swiper';
906 1040 }
907 1041
908 - $script[] = 'rtsb-imagesloaded';
909 - $script[] = 'rtsb-public';
1042 + if ( $masonry ) {
1043 + $script[] = 'masonry';
1044 + }
910 1045
1046 + $script[] = 'imagesloaded';
1047 + $script[] = Fns::optimized_handle( 'rtsb-public' );
1048 +
911 1049 foreach ( $script as $sc ) {
912 1050 wp_enqueue_script( $sc );
913 1051 }
914 1052 }
@@ -950,6 +1088,981 @@
950 1088 $products_col = absint( get_option( 'woocommerce_catalog_columns', 4 ) );
951 1089 $products_per_page = apply_filters( 'rtsb/elementor/archive/products_per_page', $products_row * $products_col );
952 1090
953 1091 return ! empty( $products_per_page ) ? $products_per_page : 12;
1092 + }
1093 + /**
1094 + * Render Filters View.
1095 + *
1096 + * @param array $templates Template name.
1097 + * @param array $settings Control settings.
1098 + *
1099 + * @return string
1100 + */
1101 + public static function filters_view( $templates, $settings ) {
1102 + global $wp;
1103 + $filters = [];
1104 + $html = null;
1105 + $reset = ! empty( $settings['reset_btn'] );
1106 + $reset_mode = 'show' === $settings['reset_btn_behavior'] ? ' show-reset' : ' ondemand-reset';
1107 + $reset_text = ! empty( $settings['reset_btn_text'] ) ? $settings['reset_btn_text'] : esc_html__( 'Reset Filter', 'shopbuilder' );
1108 + $scroll_mode = ! empty( $settings['enable_scroll'] ) ? ' has-scroll' : ' no-scroll';
1109 + $scroll_height = ! empty( $settings['enable_scroll'] ) ? $settings['scroll_height']['size'] : 300;
1110 + $has_mobile_toggle = ! empty( $settings['filter_mobile_toggle'] );
1111 + $toggle_class = $has_mobile_toggle ? ' default-filter-has-toggle' : ' default-filter-no-toggle';
1112 + $mobile_toggle_text = ! empty( $settings['filter_mobile_toggle_text'] ) ? $settings['filter_mobile_toggle_text'] : esc_html__( 'Click to Filter', 'shopbuilder' );
1113 + $scroll_attr = '';
1114 +
1115 + if ( ! empty( $settings['enable_scroll'] ) ) {
1116 + $scroll_attr = ' style="--rtsb-filter-scroll-height: ' . absint( $scroll_height ) . 'px;"';
1117 + }
1118 +
1119 + foreach ( $settings['filter_types'] as $key => $item ) {
1120 + if ( ! defined( 'RTWPVS_PLUGIN_FILE' ) && in_array( $item['input_type_all'], [ 'color', 'button', 'image' ], true ) ) {
1121 + $item['input_type_all'] = 'checkbox';
1122 + }
1123 + $filters[] = [
1124 + 'filter_title' => $item['filter_title'],
1125 + 'filter_items' => $item['filter_items'],
1126 + 'filter_attr' => $item['filter_attr'],
1127 + 'input_type' => 'product_attr' === $item['filter_items'] ? $item['input_type_all'] : $item['input_type'],
1128 + ];
1129 +
1130 + if ( 'rating_filter' === $item['filter_items'] ) {
1131 + $filters[ $key ]['template'] = $templates['rating'];
1132 + } elseif ( 'price_filter' === $item['filter_items'] ) {
1133 +
1134 + $filters[ $key ]['input_type'] = 'slider';
1135 + $filters[ $key ]['template'] = $templates['price'];
1136 + } else {
1137 + $filters[ $key ]['template'] = 'product_attr' === $item['filter_items'] ? $templates[ $item['input_type_all'] ] : $templates[ $item['input_type'] ];
1138 + }
1139 + }
1140 +
1141 + if ( '' === get_option( 'permalink_structure' ) ) {
1142 + $form_action = remove_query_arg( [ 'page', 'paged', 'product-page' ], add_query_arg( $wp->query_string, '', home_url( $wp->request ) ) );
1143 + } else {
1144 + $form_action = preg_replace( '%\/page/[0-9]+%', '', home_url( trailingslashit( $wp->request ) ) );
1145 + }
1146 + $args = [
1147 + 'filters' => $filters,
1148 + 'scroll_mode' => $scroll_mode,
1149 + 'reset_mode' => $reset_mode,
1150 + 'reset' => $reset,
1151 + 'scroll_attr' => $scroll_attr,
1152 + 'toggle_class' => $toggle_class,
1153 + 'reset_text' => $reset_text,
1154 + 'settings' => $settings,
1155 + 'form_action' => $form_action,
1156 + ];
1157 +
1158 + $html .= '<div class="rtsb-default-archive-filters' . esc_attr( $toggle_class ) . '">';
1159 +
1160 + if ( $has_mobile_toggle ) {
1161 + $html .= '<div class="rtsb-filter-mobile-toggle">
1162 + <button class="product-filter-toggle">
1163 + <span class="icon"><i aria-hidden="true" class="toggle-icon rtsb-icon rtsb-icon-filter"></i></span>
1164 + <span class="text reset-text">' . esc_html( $mobile_toggle_text ) . '</span>
1165 + <span></span>
1166 + </button>
1167 + </div>';
1168 + }
1169 +
1170 + $html .= Fns::load_template( $templates['layout'], $args, true );
1171 + $html .= '</div>';
1172 + return $html;
1173 + }
1174 + /**
1175 + * Get taxonomy type.
1176 + *
1177 + * @param string $tax_type Taxonomy type.
1178 + * @param string $attr_type Attribute type.
1179 + *
1180 + * @return string
1181 + */
1182 + public static function get_product_filters_tax_type( $tax_type, $attr_type = '' ) {
1183 + if ( 'product_attr' !== $tax_type ) {
1184 + return $tax_type;
1185 + }
1186 +
1187 + if ( empty( $attr_type ) ) {
1188 + return $tax_type;
1189 + }
1190 +
1191 + return $attr_type;
1192 + }
1193 + /**
1194 + * Get attribute terms.
1195 + *
1196 + * @param string $tax Taxonomy type.
1197 + * @return int[]|string|string[]|\WP_Error|\WP_Term[]
1198 + */
1199 + public static function get_product_filters_attribute_terms( $tax ) {
1200 + $cache_key = 'get_default_filter_attribute_terms_' . $tax;
1201 + if ( isset( self::$cache[ $cache_key ] ) ) {
1202 + return self::$cache[ $cache_key ];
1203 + }
1204 +
1205 + $attributes = wc_get_attribute_taxonomies();
1206 + $att_name = str_replace( 'pa_', '', $tax );
1207 + $exist = array_search( $att_name, array_column( $attributes, 'attribute_name' ), true );
1208 + if ( false === $exist ) {
1209 + self::$cache[ $cache_key ] = [];
1210 + return self::$cache[ $cache_key ];
1211 + }
1212 + $attribute = $tax;
1213 +
1214 + if ( ! is_shop() && is_product_taxonomy() ) {
1215 + $term = get_queried_object();
1216 +
1217 + if ( ! $term instanceof WP_Term ) {
1218 + return [];
1219 + }
1220 +
1221 + $args = [
1222 + 'status' => 'publish',
1223 + 'limit' => -1,
1224 + 'return' => 'ids',
1225 + ];
1226 +
1227 + if ( 'product_cat' === $term->taxonomy ) {
1228 + $args['category'] = [ $term->slug ];
1229 + }
1230 +
1231 + if ( 'product_tag' === $term->taxonomy ) {
1232 + $args['tag'] = [ $term->slug ];
1233 + }
1234 +
1235 + if ( strpos( $term->taxonomy, 'pa_' ) !== false ) {
1236 + $args['tax_query'] = [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
1237 + [
1238 + 'taxonomy' => $term->taxonomy,
1239 + 'field' => 'term_id',
1240 + 'terms' => $term->term_id,
1241 + 'operator' => 'IN',
1242 + ],
1243 + ];
1244 + }
1245 +
1246 + $query = new WC_Product_Query( $args );
1247 + $products = $query->get_products();
1248 + $attr_terms = [];
1249 +
1250 + foreach ( $products as $product_id ) {
1251 + $product_attr = wp_get_post_terms( $product_id, $attribute );
1252 +
1253 + foreach ( $product_attr as $attr ) {
1254 + $attr_terms[] = $attr;
1255 + }
1256 + }
1257 + self::$cache[ $cache_key ] = self::filter_products_array_unique( $attr_terms );
1258 + return self::$cache[ $cache_key ];
1259 + } else {
1260 + self::$cache[ $cache_key ] = get_terms(
1261 + [
1262 + 'taxonomy' => $attribute,
1263 + 'hide_empty' => false,
1264 + ]
1265 + );
1266 + return self::$cache[ $cache_key ];
1267 + }
1268 + }
1269 + /**
1270 + * Remove duplicate values from an array.
1271 + *
1272 + * @param array $array The input array.
1273 + * @param bool $keep_key_assoc Whether to keep the key associations after removing duplicates.
1274 + * @return array The array with duplicates removed.
1275 + */
1276 + public static function filter_products_array_unique( $array, $keep_key_assoc = false ) {
1277 + $duplicate_keys = [];
1278 + $tmp = [];
1279 +
1280 + foreach ( $array as $key => $val ) {
1281 + // convert objects to arrays, in_array() does not support objects.
1282 + if ( is_object( $val ) ) {
1283 + $val = (array) $val;
1284 + }
1285 +
1286 + if ( ! in_array( $val, $tmp, true ) ) {
1287 + $tmp[] = $val;
1288 + } else {
1289 + $duplicate_keys[] = $key;
1290 + }
1291 + }
1292 +
1293 + foreach ( $duplicate_keys as $key ) {
1294 + unset( $array[ $key ] );
1295 + }
1296 +
1297 + return $keep_key_assoc ? $array : array_values( $array );
1298 + }
1299 + /**
1300 + * Retrieve product categories (including hierarchical support)
1301 + *
1302 + * @param string $taxonomy Taxonomy name.
1303 + * @return int[]|string|string[]|\WP_Error|\WP_Term[]
1304 + */
1305 + public static function get_all_terms( $taxonomy ) {
1306 + $args = [
1307 + 'taxonomy' => $taxonomy,
1308 + 'suppress_filter' => true,
1309 + 'hide_empty' => false,
1310 + 'pad_counts' => true,
1311 + 'hierarchical' => true,
1312 + ];
1313 +
1314 + // Archive-scope only when the URL path itself is a term archive
1315 + // (e.g. /product-category/clothing/). On the shop page WordPress can
1316 + // resolve ?product_cat=X to a queried term, but in that case we want
1317 + // the full hierarchy to remain visible for additional filtering.
1318 + $is_term_archive_url = self::request_path_is_term_archive( $taxonomy );
1319 +
1320 + $cache_key = $is_term_archive_url
1321 + ? 'rtsb_get_terms_filter_products_archive_' . $taxonomy
1322 + : 'rtsb_get_terms_filter_products_full_' . $taxonomy;
1323 + $taxonomy_terms = wp_cache_get( $cache_key, 'shopbuilder' );
1324 +
1325 + if ( false === $taxonomy_terms ) {
1326 + $archive_term = $is_term_archive_url ? self::resolve_archive_term_from_path( $taxonomy ) : null;
1327 +
1328 + if ( $archive_term instanceof WP_Term ) {
1329 + $term_id = $archive_term->term_id;
1330 + $archive_cache_key = 'rtsb_get_terms_filter_products_' . $taxonomy . $term_id;
1331 + $taxonomy_terms = wp_cache_get( $archive_cache_key, 'shopbuilder' );
1332 + if ( false === $taxonomy_terms ) {
1333 + // Treat the URL-derived term as the root of the visible
1334 + // hierarchy so the recursive renderer in
1335 + // get_product_default_filter_list_html() (which starts
1336 + // with $parent = 0) picks it up on child-category
1337 + // archives. Clone first to avoid mutating WP's term
1338 + // object.
1339 + $current_term = clone $archive_term;
1340 + $current_term->parent = 0;
1341 + $taxonomy_terms = [ $current_term ];
1342 + $child_args = [
1343 + 'taxonomy' => $taxonomy,
1344 + 'child_of' => $term_id,
1345 + 'hide_empty' => false,
1346 + ];
1347 +
1348 + $child_terms = get_terms( $child_args );
1349 +
1350 + if ( ! empty( $child_terms ) ) {
1351 + $taxonomy_terms = array_merge( $taxonomy_terms, $child_terms );
1352 + }
1353 +
1354 + wp_cache_set( $archive_cache_key, $taxonomy_terms, 'shopbuilder' );
1355 + Cache::set_data_cache_key( $archive_cache_key );
1356 + }
1357 + } else {
1358 + $taxonomy_terms = get_terms( $args );
1359 + }
1360 +
1361 + wp_cache_set( $cache_key, $taxonomy_terms, 'shopbuilder' );
1362 + Cache::set_data_cache_key( $cache_key );
1363 + }
1364 +
1365 + return $taxonomy_terms;
1366 + }
1367 +
1368 + /**
1369 + * Resolve the archive term from the request URL path. Avoids relying on
1370 + * get_queried_object() which can return an unexpected term when
1371 + * ?product_cat=a,b,c is also present in the request.
1372 + *
1373 + * @param string $taxonomy Taxonomy slug.
1374 + *
1375 + * @return \WP_Term|null
1376 + */
1377 + private static function resolve_archive_term_from_path( $taxonomy ) {
1378 + $rewrite_slugs = self::get_taxonomy_archive_slugs( $taxonomy );
1379 +
1380 + if ( empty( $rewrite_slugs ) ) {
1381 + return null;
1382 + }
1383 +
1384 + // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Path only used for slug extraction.
1385 + $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '';
1386 + // phpcs:enable
1387 + $path = (string) wp_parse_url( $request_uri, PHP_URL_PATH );
1388 +
1389 + if ( '' === $path ) {
1390 + return null;
1391 + }
1392 +
1393 + foreach ( $rewrite_slugs as $base ) {
1394 + $base = trim( $base, '/' );
1395 + $needle = '/' . $base . '/';
1396 +
1397 + $pos = strpos( $path, $needle );
1398 + if ( false === $pos ) {
1399 + continue;
1400 + }
1401 +
1402 + $remainder = substr( $path, $pos + strlen( $needle ) );
1403 + $segments = array_values(
1404 + array_filter(
1405 + explode( '/', $remainder ),
1406 + static function ( $segment ) {
1407 + return '' !== $segment;
1408 + }
1409 + )
1410 + );
1411 +
1412 + if ( empty( $segments ) ) {
1413 + continue;
1414 + }
1415 +
1416 + $slug = sanitize_title( end( $segments ) );
1417 + $term = get_term_by( 'slug', $slug, $taxonomy );
1418 +
1419 + if ( $term instanceof WP_Term ) {
1420 + return $term;
1421 + }
1422 + }
1423 +
1424 + return null;
1425 + }
1426 +
1427 + /**
1428 + * Decide whether the current REQUEST_URI path represents a real term
1429 + * archive page for the given taxonomy. URL-based so it ignores cases
1430 + * where WordPress merely resolves a query string into a queried term
1431 + * on the shop page.
1432 + *
1433 + * @param string $taxonomy Taxonomy slug.
1434 + *
1435 + * @return bool
1436 + */
1437 + private static function request_path_is_term_archive( $taxonomy ) {
1438 + $rewrite_slugs = self::get_taxonomy_archive_slugs( $taxonomy );
1439 +
1440 + if ( empty( $rewrite_slugs ) ) {
1441 + return false;
1442 + }
1443 +
1444 + // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Path only used in match.
1445 + $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '';
1446 + // phpcs:enable
1447 + $path = (string) wp_parse_url( $request_uri, PHP_URL_PATH );
1448 +
1449 + if ( '' === $path ) {
1450 + return false;
1451 + }
1452 +
1453 + foreach ( $rewrite_slugs as $slug ) {
1454 + if ( '' === $slug ) {
1455 + continue;
1456 + }
1457 + if ( false !== strpos( $path, '/' . trim( $slug, '/' ) . '/' ) ) {
1458 + return true;
1459 + }
1460 + }
1461 +
1462 + return false;
1463 + }
1464 +
1465 + /**
1466 + * Resolve the URL slug(s) used by a taxonomy archive (e.g. product_cat
1467 + * maps to "product-category"). Falls back to common WooCommerce defaults
1468 + * when the taxonomy object is unavailable.
1469 + *
1470 + * @param string $taxonomy Taxonomy slug.
1471 + *
1472 + * @return array<string>
1473 + */
1474 + private static function get_taxonomy_archive_slugs( $taxonomy ) {
1475 + $slugs = [];
1476 +
1477 + if ( taxonomy_exists( $taxonomy ) ) {
1478 + $tax_obj = get_taxonomy( $taxonomy );
1479 + if ( $tax_obj && ! empty( $tax_obj->rewrite['slug'] ) ) {
1480 + $slugs[] = $tax_obj->rewrite['slug'];
1481 + }
1482 + }
1483 +
1484 + $defaults = [
1485 + 'product_cat' => 'product-category',
1486 + 'product_tag' => 'product-tag',
1487 + 'product_brand' => 'product-brand',
1488 + ];
1489 +
1490 + if ( isset( $defaults[ $taxonomy ] ) ) {
1491 + $slugs[] = $defaults[ $taxonomy ];
1492 + }
1493 +
1494 + return array_unique( array_filter( $slugs ) );
1495 + }
1496 + /**
1497 + * Generates an error message block.
1498 + *
1499 + * @param array $title The title array of the error message block.
1500 + * @param string $wrapper Additional CSS class for the wrapper div.
1501 + *
1502 + * @return string The HTML representation of the error message block.
1503 + */
1504 + public static function filter_error_message( $title, $wrapper ) {
1505 + $html = '<div class="rtsb-product-default-filters ' . esc_attr( $wrapper ) . '">';
1506 + $html .= self::get_default_filter_title( $title );
1507 + $html .= '<div class="default-filter-content">';
1508 + $html .= '<p class="no-filter">' . esc_html__( 'Nothing found.', 'shopbuilder' ) . '</p>';
1509 + $html .= '</div>';
1510 + $html .= '</div>';
1511 +
1512 + return $html;
1513 + }
1514 + /**
1515 + * Get filter title.
1516 + *
1517 + * @param array $title Filter title.
1518 + * @return string
1519 + */
1520 + public static function get_default_filter_title( $title ) {
1521 + $html = '';
1522 +
1523 + if ( empty( $title['title'] ) ) {
1524 + return $html;
1525 + }
1526 +
1527 + $html .= '<div class="default-filter-title-wrapper">';
1528 +
1529 + $html .= '<h3 class="widget-title rtsb-d-flex rtsb-align-items-center"><span class="title">' . $title['title'] . '</span></h3>';
1530 +
1531 + $html .= '</div>';
1532 +
1533 + return apply_filters( 'rtsb/elementor/default/filter/title', $html, $title );
1534 + }
1535 + /**
1536 + * Recursive function to generate the filter list with hierarchy.
1537 + *
1538 + * @param array $data Array of required data.
1539 + * @param string $input Type of input field for the filter (e.g., checkbox).
1540 + * @param array $additional_data Data for additional filtering.
1541 + * @param int $parent ID of the parent term (default: 0).
1542 + *
1543 + * @return string The generated HTML for the product filter list.
1544 + */
1545 + public static function get_product_default_filter_list_html( $data, $input = 'checkbox', $additional_data = [], $parent = 0 ) {
1546 +
1547 + $html = '';
1548 + $default_tax = [];
1549 + $is_attribute = ! empty( $data['is_attribute'] );
1550 +
1551 + $param_key = isset( $additional_data['taxonomy'] ) ? $additional_data['taxonomy'] : '';
1552 + if ( $param_key && 0 === strpos( $param_key, 'pa_' ) ) {
1553 + $param_key = 'filter_' . substr( $param_key, 3 );
1554 + }
1555 +
1556 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1557 + $raw_values = ( $param_key && isset( $_GET[ $param_key ] ) ) ? wc_clean( wp_unslash( $_GET[ $param_key ] ) ) : '';
1558 + $active_option = $raw_values ? array_map( 'urldecode', explode( ',', $raw_values ) ) : [];
1559 + $counter = 0;
1560 +
1561 + if ( $is_attribute ) {
1562 + list(
1563 + 'filter_name' => $filter_name,
1564 + 'base_link' => $base_link
1565 + ) = self::get_product_filters_attribute_term_info( $additional_data['taxonomy'] );
1566 + }
1567 +
1568 + $html .= '<ul class="product-default-filters input-type-' . esc_attr( $input ) . '">';
1569 +
1570 + foreach ( $data['taxonomy'] as $tax ) {
1571 + if ( $tax->parent !== $parent ) {
1572 + continue;
1573 + }
1574 +
1575 + if ( $is_attribute ) {
1576 + $tax_link = remove_query_arg( $filter_name, $base_link );
1577 + $tax_link = add_query_arg( $filter_name, $tax->slug, $tax_link );
1578 + } else {
1579 + $tax_link = get_term_link( $tax );
1580 + }
1581 +
1582 + $unique_id = substr( uniqid(), -4 );
1583 +
1584 + $decoded_slug = urldecode( $tax->slug );
1585 + $is_active = in_array( $decoded_slug, $active_option, true ) || in_array( $tax->slug, $active_option, true );
1586 + $active_tax = $is_active ? ' checked' : '';
1587 + $active_tax_parent = $is_active ? ' active' : '';
1588 + $term_children = in_array( $tax->taxonomy, [ 'product_cat', 'product_brand' ], true ) ? get_term_children( $tax->term_id, $tax->taxonomy ) : [];
1589 + $taxonomy = $tax->taxonomy;
1590 + $product_count = self::get_visible_term_count( $tax->taxonomy, $tax->term_id );
1591 +
1592 + // On a product taxonomy archive (category/tag/attribute page) scope the
1593 + // count to the current archive - only products that are in THIS archive
1594 + // AND have this term. This makes the displayed count reflect the current
1595 + // category context and lets terms that are empty in this archive be
1596 + // dropped from the list.
1597 + if ( is_product_taxonomy() || 'archive' === apply_filters( 'rtsb/builder/set/current/page/type', '' ) ) {
1598 + $product_count = self::count_tax_data( $tax, $product_count );
1599 + }
1600 +
1601 + // Show Empty Categories.
1602 + if ( empty( $data['show_empty_terms'] ) && 0 === $product_count ) {
1603 + continue;
1604 + }
1605 +
1606 + $html .= '<li class="rtsb-default-filter-term-item term-item-' . absint( $tax->term_id ) . esc_attr( $term_children ? ' term-has-children' : '' ) . ( ! $data['show_child'] ? ' child-terms-hidden' : '' ) . '">
1607 + <div class="rtsb-default-filter-group' . esc_attr( $active_tax_parent ) . '">
1608 + <input type="' . esc_attr( $input ) . '" class="rtsb-default-filter-trigger rtsb-' . esc_attr( $input . '-filter rtsb-term-' . $tax->slug . $active_tax ) . '" name="rtsb-filter-' . esc_attr( $taxonomy ) . '[]" id="rtsb-default-term-filter-' . $unique_id . absint( $tax->term_id ) . '" value="' . esc_attr( $tax->slug ) . '" ' . esc_attr( $active_tax ) . '>
1609 + <label class="rtsb-default-' . esc_attr( $input ) . '-filter-label" for="rtsb-default-term-filter-' . $unique_id . absint( $tax->term_id ) . '">
1610 + <span class="name">' . esc_html( $tax->name ) . '</span>
1611 + </label>
1612 + <div class="rtsb-product-count">(' . esc_html( $product_count ) . ')</div>
1613 + ' . ( ! empty( $term_children ) && $data['show_child'] ? '<i class="rtsb-plus-icon"></i>' : '' ) . '
1614 + </div>';
1615 +
1616 + // Show Sub-categories.
1617 + if ( $data['show_child'] ) {
1618 + $children = self::get_product_default_filter_list_html( $data, $input, $additional_data, $tax->term_id );
1619 +
1620 + if ( ! empty( $children ) ) {
1621 + $html .= '<div class="filter-child">' . $children . '</div>';
1622 + }
1623 + }
1624 +
1625 + $counter++;
1626 +
1627 + $html .= '</li>';
1628 + }
1629 +
1630 + $html .= '</ul>';
1631 +
1632 + // Check if the output contains any child elements.
1633 + $has_children = strpos( $html, '<li class="rtsb-default-filter-term-item' ) !== false;
1634 +
1635 + if ( ! $has_children ) {
1636 + // Remove the outer <ul> if there are no child elements.
1637 + $html = '';
1638 + }
1639 + return $html;
1640 + }
1641 + /**
1642 + * Get attribute term info.
1643 + *
1644 + * @param string $tax Taxonomy type.
1645 + * @return array
1646 + */
1647 + public static function get_product_filters_attribute_term_info( $tax ) {
1648 + $result = [];
1649 + $attributes = wc_get_attribute_taxonomies();
1650 +
1651 + foreach ( $attributes as $attr ) {
1652 + if ( str_replace( 'pa_', '', $tax ) === $attr->attribute_name ) {
1653 + $term_type = $attr->attribute_type;
1654 + $attribute_slug = $attr->attribute_name;
1655 + $attribute = wc_attribute_taxonomy_name( $attr->attribute_name );
1656 + break;
1657 + }
1658 + }
1659 +
1660 + $filter_name = 'filter_' . wc_attribute_taxonomy_slug( $attribute_slug );
1661 +
1662 + $result['attribute'] = $attribute ?? '';
1663 + $result['term_type'] = $term_type ?? '';
1664 + $result['attribute_slug'] = $attribute_slug ?? '';
1665 + $result['filter_name'] = $filter_name;
1666 + $result['base_link'] = self::get_base_url();
1667 +
1668 + return $result;
1669 + }
1670 + /**
1671 + * Get base URL.
1672 + *
1673 + * @return string|null
1674 + */
1675 + public static function get_base_url() {
1676 + global $wp;
1677 + return home_url( add_query_arg( [], $wp->request ) );
1678 + }
1679 + /**
1680 + * Count the number of occurrences for a specific term in a taxonomy for the current queried object.
1681 + *
1682 + * @param object|array $tax The term object for the taxonomy.
1683 + * @param int $count The initial count value.
1684 + *
1685 + * @return int The updated count value.
1686 + */
1687 + public static function count_tax_data( $tax, $count ) {
1688 + $page = get_queried_object();
1689 +
1690 + // Scoping only applies on a real taxonomy archive where the queried
1691 + // object is a term. On other contexts (e.g. the shop page) the queried
1692 + // object is not a term, so keep the original (global) count.
1693 + if ( ! $page instanceof \WP_Term ) {
1694 + return $count;
1695 + }
1696 +
1697 + if ( is_array( $tax ) && strpos( $tax['taxonomy'], 'attribute_' ) !== false ) {
1698 + $taxonomy = str_replace( 'attribute_', '', $tax['taxonomy'] );
1699 + $term_id = $tax['term_id'];
1700 + } else {
1701 + $taxonomy = $tax->taxonomy;
1702 + $term_id = $tax->term_id;
1703 + }
1704 +
1705 + if ( strpos( $page->taxonomy, 'pa_' ) !== false ) {
1706 + $args['status'] = 'publish';
1707 + $args['limit'] = -1;
1708 + $args['return'] = 'ids';
1709 + $args['tax_query'] = [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
1710 + 'relation' => 'AND',
1711 + [
1712 + 'taxonomy' => $page->taxonomy,
1713 + 'field' => 'term_id',
1714 + 'terms' => $page->term_id,
1715 + 'operator' => 'IN',
1716 + ],
1717 + [
1718 + 'taxonomy' => $tax->taxonomy,
1719 + 'field' => 'term_id',
1720 + 'terms' => $tax->term_id,
1721 + 'operator' => 'IN',
1722 + ],
1723 + [
1724 + 'taxonomy' => 'product_visibility',
1725 + 'terms' => [ 'exclude-from-catalog' ],
1726 + 'field' => 'name',
1727 + 'operator' => 'NOT IN',
1728 + ],
1729 + ];
1730 +
1731 + $query = new WC_Product_Query( $args );
1732 + $products = $query->get_products();
1733 +
1734 + return count( $products );
1735 + }
1736 +
1737 + // Scope the count to the current archive term (category, brand, tag, etc.)
1738 + // so it reflects only products that are in this archive AND also have the
1739 + // filter term. Works for cross-taxonomy filters (e.g. brands on a tag
1740 + // archive) and same-taxonomy filters (e.g. the Tags filter on a tag
1741 + // archive shows the intersection with the current tag).
1742 + $args = [
1743 + 'limit' => -1,
1744 + 'return' => 'ids',
1745 + 'status' => 'publish',
1746 + 'tax_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
1747 + 'relation' => 'AND',
1748 + [
1749 + 'taxonomy' => $taxonomy,
1750 + 'field' => 'term_id',
1751 + 'terms' => $term_id,
1752 + ],
1753 + [
1754 + 'taxonomy' => $page->taxonomy,
1755 + 'field' => 'term_id',
1756 + 'terms' => $page->term_id,
1757 + ],
1758 + [
1759 + 'taxonomy' => 'product_visibility',
1760 + 'terms' => [ 'exclude-from-catalog' ],
1761 + 'field' => 'name',
1762 + 'operator' => 'NOT IN',
1763 + ],
1764 + ],
1765 + ];
1766 +
1767 + $query = new WC_Product_Query( $args );
1768 + $products = $query->get_products();
1769 +
1770 + return count( $products );
1771 + }
1772 +
1773 + /**
1774 + * Get the count of visible (non-hidden) products for a given taxonomy term.
1775 + *
1776 + * Excludes products with WooCommerce visibility set to 'Hidden'
1777 + * (products assigned the 'exclude-from-catalog' term).
1778 + *
1779 + * @param string $taxonomy The taxonomy name.
1780 + * @param int $term_id The term ID.
1781 + *
1782 + * @return int Visible product count.
1783 + */
1784 + public static function get_visible_term_count( $taxonomy, $term_id ) {
1785 + static $cache = [];
1786 +
1787 + $cache_key = $taxonomy . '_' . $term_id;
1788 +
1789 + if ( isset( $cache[ $cache_key ] ) ) {
1790 + return $cache[ $cache_key ];
1791 + }
1792 +
1793 + $args = [
1794 + 'limit' => -1,
1795 + 'return' => 'ids',
1796 + 'status' => 'publish',
1797 + 'tax_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
1798 + 'relation' => 'AND',
1799 + [
1800 + 'taxonomy' => $taxonomy,
1801 + 'field' => 'term_id',
1802 + 'terms' => $term_id,
1803 + ],
1804 + [
1805 + 'taxonomy' => 'product_visibility',
1806 + 'terms' => [ 'exclude-from-catalog' ],
1807 + 'field' => 'name',
1808 + 'operator' => 'NOT IN',
1809 + ],
1810 + ],
1811 + ];
1812 +
1813 + $query = new WC_Product_Query( $args );
1814 + $count = count( $query->get_products() );
1815 +
1816 + $cache[ $cache_key ] = $count;
1817 +
1818 + return $count;
1819 + }
1820 +
1821 + /**
1822 + * Get filter HTML.
1823 + *
1824 + * @param string $filter_type Filter type.
1825 + * @param array $options Options.
1826 + * @param array $additional_data Data for additional filtering.
1827 + * @param string $input Input type.
1828 + *
1829 + * @return string The generated HTML for the attribute filter list.
1830 + */
1831 + public static function get_default_filter_html( $filter_type, $options, $additional_data = [], $input = 'checkbox' ) {
1832 + $html = '<ul class="product-filters input-type-' . esc_attr( $input ) . '" ';
1833 + $active_option = isset( $_GET[ $filter_type ] ) ? explode( ',', wc_clean( wp_unslash( $_GET[ $filter_type ] ) ) ) : []; // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1834 + $option_link = self::get_base_url() . '/' . $filter_type . '/';
1835 +
1836 + foreach ( $options as $option => $option_name ) {
1837 + $unique_id = substr( uniqid(), -6 );
1838 + $active_tax = in_array( $option, $active_option, true ) ? ' checked' : '';
1839 + $active_group = in_array( $option, $active_option, true ) ? ' active' : '';
1840 +
1841 + $html .= '
1842 + <li class="rtsb-term-item">
1843 + <div class="rtsb-default-filter-group' . esc_attr( $active_group ) . '">
1844 + <input type="' . esc_attr( $input ) . '" class="rtsb-default-filter-trigger rtsb-' . esc_attr( $input ) . '-filter rtsb-term-' . esc_attr( $option ) . esc_attr( $active_tax ) . '" name="rtsb-filter-' . esc_attr( $filter_type ) . '[]" id="rtsb-term-default-filter-' . $unique_id . '" value="' . esc_attr( $option ) . '" ' . $active_tax . '>
1845 + <label class="rtsb-default-' . esc_attr( $input ) . '-filter-label" for="rtsb-term-default-filter-' . $unique_id . '">
1846 + <span class="name">' . esc_html( $option_name ) . '</span>
1847 + </label>
1848 + </div>
1849 + </li>';
1850 + }
1851 +
1852 + $html .= '</ul>';
1853 +
1854 + return $html;
1855 + }
1856 + /**
1857 + * Get the count of products with a specific star rating.
1858 + *
1859 + * @param int $star_rating The star rating value.
1860 + *
1861 + * @return int
1862 + */
1863 + public static function get_product_rating_count( $star_rating ) {
1864 + $args = [
1865 + 'status' => 'publish',
1866 + 'limit' => -1,
1867 + 'product_rating' => sprintf( '%.1f', $star_rating ),
1868 + ];
1869 +
1870 + if ( is_product_taxonomy() ) {
1871 + $term = get_queried_object();
1872 + $term_type = 'product_cat' === $term->taxonomy ? 'product_category_id' : 'product_tag_id';
1873 +
1874 + if ( $term instanceof WP_Term ) {
1875 + $args[ $term_type ] = [ $term->term_id ];
1876 + }
1877 + }
1878 +
1879 + $query = new WC_Product_Query( $args );
1880 + $products = $query->get_products();
1881 +
1882 + return count( $products );
1883 + }
1884 + /**
1885 + * Get attribute filter HTML.
1886 + *
1887 + * @param array $data Array of required data.
1888 + * @param string $input Type of input field for the filter (e.g., color).
1889 + *
1890 + * @return string The generated HTML for the attribute filter list.
1891 + */
1892 + public static function get_attribute_filter_list_html( $data, $input = 'color' ) {
1893 + if ( ! function_exists( 'rtwpvs' ) ) {
1894 + return '';
1895 + }
1896 +
1897 + $terms = $data['terms'];
1898 + $term_info = $data['term_info'];
1899 + $show_tooltips = $data['show_tooltips'];
1900 + $show_empty_terms = $data['show_empty_terms'];
1901 + $counter = 0;
1902 +
1903 + $html = '<ul class="product-default-filters rtsb-terms-wrapper ' . esc_attr( $term_info['type'] ) . '-variable-wrapper' . esc_attr( $data['show_label'] ) . '">';
1904 +
1905 + foreach ( $terms as $attr_term ) {
1906 + $count = self::get_visible_term_count( $attr_term->taxonomy, $attr_term->term_id );
1907 + $term_link = remove_query_arg( $term_info['filter_name'], $term_info['base_link'] );
1908 + $term_link = add_query_arg( $term_info['filter_name'], $attr_term->slug, $term_link );
1909 + $unique_id = substr( uniqid(), -6 );
1910 + $name = 'term-' . wc_variation_attribute_name( $term_info['attribute'] ) . '-' . $unique_id;
1911 + $decoded_slug = urldecode( $attr_term->slug );
1912 + $is_selected = in_array( $decoded_slug, $term_info['current_filter'], true ) || in_array( $attr_term->slug, $term_info['current_filter'], true );
1913 + $selected_class = $is_selected ? ' selected' : '';
1914 + $selected_item = $is_selected ? ' checked' : '';
1915 + $active_class = $is_selected ? ' active' : '';
1916 +
1917 + $args = [
1918 + 'id' => $name,
1919 + 'name' => $attr_term->name,
1920 + 'link' => $term_link,
1921 + 'type' => $term_info['type'],
1922 + 'term_id' => $attr_term->term_id,
1923 + 'term_slug' => $attr_term->slug,
1924 + 'taxonomy' => wc_variation_attribute_name( $term_info['attribute'] ),
1925 + 'tooltips' => $show_tooltips,
1926 + 'attribute_name' => $term_info['attribute'],
1927 + 'selected_item' => $selected_item,
1928 + ];
1929 +
1930 + if ( 'archive' === apply_filters( 'rtsb/builder/set/current/page/type', '' ) && ! empty( $data['count_display'] ) && 'block' === $data['count_display'] ) {
1931 + $count = self::count_tax_data( $args, $count );
1932 + }
1933 +
1934 + $args['count'] = $count;
1935 +
1936 + if ( ! $show_empty_terms && 0 === $count ) {
1937 + continue;
1938 + }
1939 +
1940 + $html .= '<li class="rtsb-default-filter-term-item term-item-' . esc_attr( $attr_term->term_id ) . ' rtsb-term rtsb-default-' . esc_attr( $input ) . '-term ' . esc_attr( $term_info['type'] ) . '-variable-term-' . esc_attr( $attr_term->slug ) . esc_attr( $selected_class ) . '">';
1941 + $html .= '<div class="rtsb-default-filter-group' . esc_attr( $active_class ) . '">';
1942 + $html .= self::get_input_type_html( $input, $args );
1943 +
1944 + $counter++;
1945 +
1946 + $html .= '</div>';
1947 + $html .= '</li>';
1948 + }
1949 +
1950 + $html .= '</ul>';
1951 +
1952 + return $html;
1953 + }
1954 + /**
1955 + * Get input type HTML.
1956 + *
1957 + * @param string $input Input type.
1958 + * @param array $args Args.
1959 + * @return mixed|string|null
1960 + */
1961 + public static function get_input_type_html( $input, $args ) {
1962 + $html = '';
1963 + $count = $args['count'];
1964 +
1965 + switch ( $input ) {
1966 + case 'color':
1967 + $color = sanitize_hex_color( get_term_meta( $args['term_id'], 'product_attribute_color', true ) );
1968 +
1969 + $html .= sprintf(
1970 + // phpcs:ignore Generic.Strings.UnnecessaryStringConcat.Found
1971 + '<input type="checkbox" id="' . esc_attr( $args['id'] ) . '" value="' . esc_attr( $args['term_slug'] ) . '" class="rtsb-attr-hidden-field" name="rtsb-filter-' . esc_attr( $args['attribute_name'] ) . '[]' . '" ' . esc_attr( $args['selected_item'] ) . '/><label for="%s" title="' . esc_attr( $args['name'] ) . '" class="rtsb-default-filter-trigger rtsb-attr-filter rtsb-color-filter rtsb-term-span rtsb-term-span-%s%s"><span class="default-filter-attr-color" style="background-color:%s;"></span><span class="default-filter-attr-name">%s</span></label><div class="rtsb-count">(%s)</div>',
1972 + esc_attr( $args['id'] ),
1973 + esc_attr( $args['type'] ),
1974 + $args['tooltips'] ? esc_attr( ' tipsy' ) : '',
1975 + esc_attr( $color ),
1976 + esc_html( $args['name'] ),
1977 + absint( $count )
1978 + );
1979 + break;
1980 +
1981 + case 'image':
1982 + if ( ! function_exists( 'rtwpvs' ) ) {
1983 + return $html;
1984 + }
1985 +
1986 + $attachment_id = absint( get_term_meta( $args['term_id'], 'product_attribute_image', true ) );
1987 + $image_size = rtwpvs()->get_option( 'attribute_image_size' );
1988 + $image_url = wp_get_attachment_image_url( $attachment_id, apply_filters( 'rtsb_product_attribute_image_size', $image_size ) );
1989 +
1990 + $html .= sprintf(
1991 + // phpcs:ignore Generic.Strings.UnnecessaryStringConcat.Found
1992 + '<input type="checkbox" id="' . esc_attr( $args['id'] ) . '" value="' . esc_attr( $args['term_slug'] ) . '" class="rtsb-attr-hidden-field" name="rtsb-filter-' . esc_attr( $args['attribute_name'] ) . '[]' . '" ' . esc_attr( $args['selected_item'] ) . '/><label for="%s" title="' . esc_attr( $args['name'] ) . '" class="rtsb-default-filter-trigger rtsb-image-filter rtsb-term-span rtsb-term-span-%s%s"><img class="rtsb-default-attr-filter" alt="%s" src="%s" /></label>',
1993 + esc_attr( $args['id'] ),
1994 + esc_attr( $args['type'] ),
1995 + $args['tooltips'] ? esc_attr( ' tipsy' ) : '',
1996 + esc_attr( $args['name'] ),
1997 + esc_url( $image_url )
1998 + );
1999 + break;
2000 +
2001 + case 'button':
2002 + $html .= sprintf(
2003 + // phpcs:ignore Generic.Strings.UnnecessaryStringConcat.Found
2004 + '<input id="' . esc_attr( $args['id'] ) . '" type="checkbox" value="' . esc_attr( $args['term_slug'] ) . '" class="rtsb-attr-hidden-field" name="rtsb-filter-' . esc_attr( $args['attribute_name'] ) . '[]' . '" ' . esc_attr( $args['selected_item'] ) . '/><label for="%s" title="' . esc_attr( $args['name'] ) . '" class="rtsb-default-filter-trigger rtsb-attr-filter rtsb-button-filter rtsb-term-span rtsb-term-span-%s%s"><span class="default-filter-attr-name">%s</span><div class="rtsb-count">(%s)</div></label>',
2005 + esc_attr( $args['id'] ),
2006 + esc_attr( $args['type'] ),
2007 + $args['tooltips'] ? esc_attr( ' tipsy' ) : '',
2008 + esc_html( $args['name'] ),
2009 + absint( $count )
2010 + );
2011 + break;
2012 +
2013 + default:
2014 + }
2015 +
2016 + return apply_filters( 'rtsb/elementor/default/filter/get_input_type_html', $html );
2017 + }
2018 + /**
2019 + * Get reset button.
2020 + *
2021 + * @param string $btn_text Button text.
2022 + * @param array $settings Settings array.
2023 + *
2024 + * @return string
2025 + */
2026 + public static function get_default_filter_reset_button( $btn_text, $settings ) {
2027 + $html = '';
2028 +
2029 + ob_start();
2030 +
2031 + $is_editor = ! empty( \Elementor\Plugin::$instance->editor ) && \Elementor\Plugin::$instance->editor->is_edit_mode();
2032 + $active = ( $is_editor || ! empty( $_SERVER['QUERY_STRING'] ) ) ? ' active' : '';
2033 + $reset = ! empty( $settings['reset_btn'] );
2034 + $apply_text = ! empty( $settings['apply_filters_btn_text'] ) ? $settings['apply_filters_btn_text'] : esc_html__( 'Apply Filters', 'shopbuilder' );
2035 + $apply_icon = ! empty( $settings['apply_filters_btn_icon'] ) ? $settings['apply_filters_btn_icon'] : [];
2036 + echo '<div class="default-filter-btn-wrapper">';
2037 + ?>
2038 + <?php if ( $settings['show_filter_btn'] ) { ?>
2039 + <div class="rtsb-product-default-filters rtsb-apply-filters-btn">
2040 + <div class="reset-btn-item">
2041 + <button class="rtsb-apply-filters init">
2042 + <span class="icon"><?php Fns::print_html( Fns::icons_manager( $apply_icon, 'icon-default' ) ); ?></span>
2043 + <span class="text reset-text"><?php echo esc_html( $apply_text ); ?></span>
2044 + <span></span>
2045 + </button>
2046 + </div>
2047 + </div>
2048 + <?php } ?>
2049 + <?php
2050 + if ( $reset ) {
2051 + ?>
2052 + <div class="rtsb-product-default-filters rtsb-reset<?php echo esc_attr( $active ); ?>">
2053 + <div class="reset-btn-item">
2054 + <button class="product-default-filter-reset">
2055 + <span class="text reset-text"><?php echo esc_html( $btn_text ); ?></span>
2056 + <span></span>
2057 + </button>
2058 + </div>
2059 + </div>
2060 + <?php
2061 + }
2062 + echo '</div>';
2063 +
2064 + $html .= ob_get_clean();
2065 +
2066 + return $html;
954 2067 }
955 2068 }