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 +1111 -46 2.2.13.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'] ),
@@ -233,11 +262,13 @@
233 262
234 263 $queried_obj = get_queried_object();
235 264
236 265 if ( is_shop() || is_product_taxonomy() ) {
237 - $data['posts_per_page'] = self::get_products_per_page();
238 - $data['order_by'] = self::get_data( $data, 'rtsb_orderby', $data['order_by'] );
239 - $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'] );
240 271 }
241 272
242 273 if ( ! is_shop() && is_product_taxonomy() ) {
243 274 if ( ! empty( $queried_obj->taxonomy ) ) {
@@ -248,8 +279,16 @@
248 279 $data['queried_term'] = esc_html( $queried_obj->term_id );
249 280 }
250 281 }
251 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 +
252 291 return apply_filters( 'rtsb/elementor/render/meta_dataset_final', $data, $meta, $raw_settings );
253 292 }
254 293
255 294 /**
@@ -273,8 +312,12 @@
273 312 'comparison_button' => ! empty( $meta['comparison_button'] ),
274 313 'quick_view_button' => ! empty( $meta['quick_view_button'] ),
275 314 'show_rating' => ! empty( $meta['show_rating'] ),
276 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' ),
277 320 'cart_icon' => self::get_data( $meta, 'cart_icon', [] ),
278 321 'wishlist_icon' => self::get_data( $meta, 'wishlist_icon', [] ),
279 322 'wishlist_icon_added' => self::get_data( $meta, 'wishlist_icon_added', [] ),
280 323 'comparison_icon' => self::get_data( $meta, 'comparison_icon', [] ),
@@ -291,9 +334,13 @@
291 334 );
292 335
293 336 $queried_obj = get_queried_object();
294 337
295 - 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() ) {
296 343 if ( ! empty( $queried_obj->taxonomy ) ) {
297 344 $data['queried_tax'] = esc_html( $queried_obj->taxonomy );
298 345 }
299 346
@@ -301,8 +348,16 @@
301 348 $data['queried_term'] = esc_html( $queried_obj->term_id );
302 349 }
303 350 }
304 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 +
305 360 return apply_filters( 'rtsb/elementor/render/archive_meta_dataset_final', $data, $meta );
306 361 }
307 362
308 363 /**
@@ -341,9 +396,16 @@
341 396
342 397 if ( ! empty( $settings['single_category'] ) ) {
343 398 $visibility[] = 'single-cat';
344 399 }
400 + if ( ! empty( $settings['show_brands'] ) ) {
401 + $visibility[] = 'brands';
402 + }
345 403
404 + if ( ! empty( $settings['single_brand'] ) ) {
405 + $visibility[] = 'single-brand';
406 + }
407 +
346 408 if ( ! empty( $settings['show_swatches'] ) ) {
347 409 $visibility[] = 'swatches';
348 410 }
349 411
@@ -409,13 +471,23 @@
409 471 * @return int
410 472 */
411 473 public static function default_columns( $layout ) {
412 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':
413 481 case 'grid-layout2':
414 482 case 'slider-layout2':
483 + case 'rtsb-coupon-layout1':
484 + case 'rtsb-coupon-layout2':
485 + case 'rtsb-coupon-layout3':
415 486 $columns = 3;
416 487 break;
417 -
488 + case 'rtsb-post-list-layout1':
489 + case 'rtsb-post-list-layout2':
418 490 case 'list-layout1':
419 491 case 'list-layout2':
420 492 case 'list-layout3':
421 493 case 'list-layout7':
@@ -421,9 +493,13 @@
421 493 case 'list-layout7':
422 494 case 'list-layout5':
423 495 $columns = 1;
424 496 break;
425 -
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':
426 502 case 'list-layout4':
427 503 case 'list-layout6':
428 504 case 'slider-layout5':
429 505 case 'slider-layout8':
@@ -428,9 +504,10 @@
428 504 case 'slider-layout5':
429 505 case 'slider-layout8':
430 506 $columns = 2;
431 507 break;
432 -
508 + case 'rtsb-logo-layout1':
509 + case 'rtsb-logo-layout2':
433 510 case 'grid-layout6':
434 511 case 'grid-layout9':
435 512 $columns = 5;
436 513 break;
@@ -481,9 +558,9 @@
481 558
482 559 $animation = $metas['hover_animation'];
483 560
484 561 if ( ! $is_cat ) {
485 - $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';
486 563 $animation .= ' gallery-hover-' . ( ! empty( $settings['gallery_hover_animation'] ) ? $settings['gallery_hover_animation'] : 'fade' );
487 564 }
488 565
489 566 $badge_class = [
@@ -504,14 +581,17 @@
504 581
505 582 if ( in_array( 'single-cat', $metas['visibility'], true ) ) {
506 583 $classes .= ' show-single-cat';
507 584 }
585 + if ( in_array( 'single-brand', $metas['visibility'], true ) ) {
586 + $classes .= ' show-single-brand';
587 + }
508 588
509 589 if ( ! $is_cat ) {
510 590 $action_btn_classes = [
511 - 'show_compare_text' => 'has-compare-text',
512 - 'show_quick_view_text' => 'has-quick-view-text',
513 - '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',
514 594 'show_compare_icon' => 'no-compare-icon',
515 595 'show_quick_view_icon' => 'no-quick-view-icon',
516 596 'show_wishlist_icon' => 'no-wishlist-icon',
517 597 'show_quick_checkout_icon' => 'no-quick_checkout-icon',
@@ -544,9 +624,9 @@
544 624 *
545 625 * @return string
546 626 */
547 627 public static function filter_layout( $layout, $template = '' ) {
548 - $allowed_patterns = apply_filters( 'rtsb/elementor/custom_layout', [ 'grid', 'list', 'slider', 'category', 'toyup', 'zilly' ] );
628 + $allowed_patterns = apply_filters( 'rtsb/elementor/custom_layout', [ 'grid', 'list', 'slider', 'category', 'toyup', 'zilly', 'rtsb' ] );
549 629 $layout_part = explode( '-', $layout );
550 630 $default = 'grid-layout1';
551 631
552 632 if ( empty( $layout_part[0] ) ) {
@@ -553,30 +633,15 @@
553 633 return $default;
554 634 }
555 635
556 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 +
557 641 return $layout;
558 642 } else {
559 - if ( ! empty( $template ) ) {
560 - $grid = preg_match( '/grid/', $template );
561 - $list = preg_match( '/list/', $template );
562 - $slider = preg_match( '/slider/', $template );
563 - $category = preg_match( '/category/', $template );
564 -
565 - if ( $grid ) {
566 - return $default;
567 - } elseif ( $list ) {
568 - return 'list-layout1';
569 - } elseif ( $slider ) {
570 - return 'slider-layout1';
571 - } elseif ( $category ) {
572 - return 'category-layout1';
573 - } else {
574 - return $default;
575 - }
576 - } else {
577 - return $default;
578 - }
643 + return ! empty( $template ) ? self::set_default_layout( $layout ) : $default;
579 644 }
580 645 }
581 646
582 647 /**
@@ -588,10 +653,13 @@
588 653 */
589 654 public static function prepare_row_classes( $settings = [] ) {
590 655 $masonry_class = null;
591 656 $classes = 'rtsb-row rtsb-content-loader element-loading';
592 - $loader_class = ' rtsb-pre-loader';
593 657
658 + $loader_class = '';
659 + if ( Fns::enable_loader() ) {
660 + $loader_class = ' rtsb-pre-loader';
661 + }
594 662 $raw_layout = esc_html( $settings['layout'] );
595 663 $layout = self::filter_layout( $raw_layout );
596 664 $grid_type = $settings['grid_type'];
597 665 $is_carousel = preg_match( '/slider/', $layout );
@@ -599,23 +667,30 @@
599 667 if ( preg_match( '/category/', $layout ) && ! empty( $settings['active_cat_slider'] ) ) {
600 668 $classes .= ' rtsb-slider-layout';
601 669 }
602 670
603 - if ( 'even' === $grid_type ) {
604 - $masonry_class = ' rtsb-even';
671 + if ( 'equal' === $grid_type ) {
672 + $masonry_class = ' rtsb-equal';
605 673 } elseif ( 'masonry' === $grid_type ) {
606 674 $masonry_class = ' rtsb-masonry';
675 + } else {
676 + $masonry_class = ' rtsb-even';
607 677 }
608 678
609 679 if ( ! rtsb()->has_pro() || $is_carousel ) {
610 680 $masonry_class = ' rtsb-even';
611 681 }
682 +
612 683 if ( $is_carousel && ! empty( $settings['raw_settings']['always_show_nav'] ) ) {
613 684 $classes .= ' always-show-nav';
614 685 }
686 +
615 687 if ( ! empty( $settings['raw_settings']['inner_slider_always_show_nav'] ) ) {
616 688 $classes .= ' inner-slider-always-show-nav';
617 689 }
690 + if ( empty( $settings['raw_settings']['cols_mobile'] ) ) {
691 + $classes .= ' rtsb-mobile-flex-row';
692 + }
618 693
619 694 $classes .= ' rtsb-' . $layout . $masonry_class . $loader_class;
620 695
621 696 return apply_filters( 'rtsb/elementor/row/classes', $classes, $settings );
@@ -640,11 +715,12 @@
640 715 return $has_filters ? self::pagination_data( $settings, $template ) : '';
641 716 } elseif ( $is_cat ) {
642 717 return '';
643 718 } else {
644 - $ajax_pagination = ! empty( $settings['show_pagination'] ) && 'pagination' !== $settings['pagination_type'];
645 - $page = Fns::product_filters_has_ajax( apply_filters( 'rtsb/builder/set/current/page/type', '' ) );
646 - $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 );
647 723
648 724 return $condition ? self::pagination_data( $settings, $template ) : '';
649 725 }
650 726 }
@@ -678,9 +754,9 @@
678 754 $d_col = 0 === $meta['d_cols'] ? self::default_columns( $meta['layout'] ) : $meta['d_cols'];
679 755 $t_col = 0 === $meta['t_cols'] ? 2 : $meta['t_cols'];
680 756 $m_col = 0 === $meta['m_cols'] ? 1 : $meta['m_cols'];
681 757
682 - if ( \Elementor\Plugin::$instance->breakpoints->has_custom_breakpoints() ) {
758 + if ( Plugin::$instance->breakpoints->has_custom_breakpoints() ) {
683 759 // Widescreen.
684 760 $w_col = self::get_data( $settings, 'cols_widescreen' );
685 761 $w_col = 0 === $w_col ? self::default_columns( $meta['layout'] ) : $w_col;
686 762 $w_group = self::get_data( $settings, 'cols_group_widescreen', 1 );
@@ -711,8 +787,11 @@
711 787 $breakpoints = [
712 788 0 => [
713 789 'slidesPerView' => absint( $m_col ),
714 790 'slidesPerGroup' => absint( $meta['m_group'] ),
791 + 'grid' => [
792 + 'rows' => 1,
793 + ],
715 794 'pagination' => [
716 795 'dynamicBullets' => $has_dynamic_dots,
717 796 ],
718 797 ],
@@ -888,9 +967,11 @@
888 967
889 968 if ( $is_carousel ) {
890 969 $loader_class = ' full-op';
891 970 }
892 -
971 + if ( ! Fns::enable_loader() ) {
972 + return '';
973 + }
893 974 return '<div class="rtsb-elements-loading rtsb-ball-clip-rotate"><div></div></div>';
894 975 }
895 976
896 977 /**
@@ -931,10 +1012,11 @@
931 1012 *
932 1013 * @return void
933 1014 */
934 1015 public static function register_scripts( $scripts ) {
935 - $caro = false;
936 - $script = [];
1016 + $caro = false;
1017 + $masonry = false;
1018 + $script = [];
937 1019
938 1020 $script[] = 'jquery';
939 1021
940 1022 foreach ( $scripts as $sc => $value ) {
@@ -941,8 +1023,12 @@
941 1023 if ( ! empty( $sc ) ) {
942 1024 if ( 'isCarousel' === $sc ) {
943 1025 $caro = $value;
944 1026 }
1027 +
1028 + if ( 'isMasonry' === $sc ) {
1029 + $masonry = $value;
1030 + }
945 1031 }
946 1032 }
947 1033
948 1034 if ( count( $scripts ) ) {
@@ -952,11 +1038,15 @@
952 1038 if ( $caro ) {
953 1039 $script[] = 'swiper';
954 1040 }
955 1041
956 - $script[] = 'rtsb-imagesloaded';
957 - $script[] = 'rtsb-public';
1042 + if ( $masonry ) {
1043 + $script[] = 'masonry';
1044 + }
958 1045
1046 + $script[] = 'imagesloaded';
1047 + $script[] = Fns::optimized_handle( 'rtsb-public' );
1048 +
959 1049 foreach ( $script as $sc ) {
960 1050 wp_enqueue_script( $sc );
961 1051 }
962 1052 }
@@ -998,6 +1088,981 @@
998 1088 $products_col = absint( get_option( 'woocommerce_catalog_columns', 4 ) );
999 1089 $products_per_page = apply_filters( 'rtsb/elementor/archive/products_per_page', $products_row * $products_col );
1000 1090
1001 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;
1002 2067 }
1003 2068 }