PluginProbe
ShopBuilder – WooCommerce Builder For Elementor / 2.5.0
ShopBuilder – WooCommerce Builder For Elementor v2.5.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 +49 -1184 3.4.2 → 2.5.0 View file →
@@ -8,13 +8,10 @@
8 8 */
9 9
10 10 namespace RadiusTheme\SB\Elementor\Helper;
11 11
12 -use Elementor\Plugin;
13 -use RadiusTheme\SB\Helpers\Cache;
12 +use RadiusTheme\SB\Helpers\BuilderFns;
14 13 use RadiusTheme\SB\Helpers\Fns;
15 -use WC_Product_Query;
16 -use WP_Term;
17 14
18 15 // Do not allow directly accessing this file.
19 16 if ( ! defined( 'ABSPATH' ) ) {
20 17 exit( 'This script cannot be accessed directly.' );
@@ -24,33 +21,8 @@
24 21 * BuilderFns class
25 22 */
26 23 class RenderHelpers {
27 24 /**
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 - /**
53 25 * Get data.
54 26 *
55 27 * @param array|string $haystack The array or string to search for the value.
56 28 * @param string $needle The key to look for in the array or the string itself.
@@ -111,9 +83,8 @@
111 83 'order' => self::get_data( $meta, 'posts_order', 'DESC' ),
112 84 'author_in' => self::get_data( $meta, 'filter_author', [] ),
113 85 'display_by' => self::get_data( $meta, 'products_filter', 'date' ),
114 86 'categories' => self::get_data( $meta, 'filter_categories', [] ),
115 - 'brands' => self::get_data( $meta, 'filter_brands', [] ),
116 87 'tags' => self::get_data( $meta, 'filter_tags', [] ),
117 88 'attributes' => self::get_data( $meta, 'filter_attributes', [] ),
118 89 'relation' => self::get_data( $meta, 'tax_relation', 'OR' ),
119 90 'category_source' => self::get_data( $meta, 'select_source', 'product_cat' ),
@@ -136,10 +107,9 @@
136 107
137 108 // Pagination.
138 109 'pagination' => ! empty( $meta['show_pagination'] ),
139 110 'posts_loading_type' => self::get_data( $meta, 'pagination_type', 'pagination' ),
140 - 'posts_per_page' => self::get_data( $meta, 'pagination_per_page', '' ) ?: ( ! empty( $meta['posts_per_page'] ) ? absint( $meta['posts_per_page'] ) : '' ),
141 - 'visible_range' => max( 1, absint( self::get_data( $meta, 'pagination_visible_range', 5 ) ) ),
111 + 'posts_per_page' => self::get_data( $meta, 'pagination_per_page', '' ),
142 112
143 113 // Image.
144 114 'f_img' => ! empty( $meta['show_featured_image'] ),
145 115 'gallery_img' => ! empty( $meta['show_product_gallery'] ),
@@ -263,13 +233,11 @@
263 233
264 234 $queried_obj = get_queried_object();
265 235
266 236 if ( is_shop() || is_product_taxonomy() ) {
267 - if ( empty( $data['posts_per_page'] ) ) {
268 - $data['posts_per_page'] = self::get_products_per_page();
269 - }
270 - $data['order_by'] = self::get_data( $data, 'rtsb_orderby', $data['order_by'] );
271 - $data['order'] = self::get_data( $data, 'rtsb_order', $data['order'] );
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'] );
272 240 }
273 241
274 242 if ( ! is_shop() && is_product_taxonomy() ) {
275 243 if ( ! empty( $queried_obj->taxonomy ) ) {
@@ -280,20 +248,8 @@
280 248 $data['queried_term'] = esc_html( $queried_obj->term_id );
281 249 }
282 250 }
283 251
284 - if ( is_search() ) {
285 - // Ask for the unescaped term: this dataset is data, not output.
286 - // get_search_query() defaults to esc_attr(), and pre-escaping here
287 - // would be encoded a second time at the output boundary and would
288 - // corrupt the JSON payload this array is encoded into.
289 - $search_query = get_search_query( false );
290 -
291 - if ( $search_query ) {
292 - $data['search_query'] = sanitize_text_field( $search_query );
293 - }
294 - }
295 -
296 252 return apply_filters( 'rtsb/elementor/render/meta_dataset_final', $data, $meta, $raw_settings );
297 253 }
298 254
299 255 /**
@@ -317,12 +273,8 @@
317 273 'comparison_button' => ! empty( $meta['comparison_button'] ),
318 274 'quick_view_button' => ! empty( $meta['quick_view_button'] ),
319 275 'show_rating' => ! empty( $meta['show_rating'] ),
320 276 'show_pagination' => ! empty( $meta['show_pagination'] ),
321 - // The base Product Archive widget has no pagination-type control: with filters it
322 - // uses Ajax Load More (see the widget's pagination notice). Custom-layout widgets
323 - // pass their own pagination_type and use a different dataset.
324 - 'posts_loading_type' => self::get_data( $meta, 'pagination_type', 'load_more' ),
325 277 'cart_icon' => self::get_data( $meta, 'cart_icon', [] ),
326 278 'wishlist_icon' => self::get_data( $meta, 'wishlist_icon', [] ),
327 279 'wishlist_icon_added' => self::get_data( $meta, 'wishlist_icon_added', [] ),
328 280 'comparison_icon' => self::get_data( $meta, 'comparison_icon', [] ),
@@ -339,13 +291,9 @@
339 291 );
340 292
341 293 $queried_obj = get_queried_object();
342 294
343 - // Use WooCommerce's own taxonomy check (covers product_cat, product_tag
344 - // and product_brand) so the queried term is recorded on every product
345 - // taxonomy archive — Ajax load-more / filters scope to it. Mirrors the
346 - // custom-layout dataset in self::meta_dataset().
347 - if ( ! is_shop() && is_product_taxonomy() ) {
295 + if ( ! is_shop() && BuilderFns::is_archive() ) {
348 296 if ( ! empty( $queried_obj->taxonomy ) ) {
349 297 $data['queried_tax'] = esc_html( $queried_obj->taxonomy );
350 298 }
351 299
@@ -353,20 +301,8 @@
353 301 $data['queried_term'] = esc_html( $queried_obj->term_id );
354 302 }
355 303 }
356 304
357 - if ( is_search() ) {
358 - // Ask for the unescaped term: this dataset is data, not output.
359 - // get_search_query() defaults to esc_attr(), and pre-escaping here
360 - // would be encoded a second time at the output boundary and would
361 - // corrupt the JSON payload this array is encoded into.
362 - $search_query = get_search_query( false );
363 -
364 - if ( $search_query ) {
365 - $data['search_query'] = sanitize_text_field( $search_query );
366 - }
367 - }
368 -
369 305 return apply_filters( 'rtsb/elementor/render/archive_meta_dataset_final', $data, $meta );
370 306 }
371 307
372 308 /**
@@ -405,16 +341,9 @@
405 341
406 342 if ( ! empty( $settings['single_category'] ) ) {
407 343 $visibility[] = 'single-cat';
408 344 }
409 - if ( ! empty( $settings['show_brands'] ) ) {
410 - $visibility[] = 'brands';
411 - }
412 345
413 - if ( ! empty( $settings['single_brand'] ) ) {
414 - $visibility[] = 'single-brand';
415 - }
416 -
417 346 if ( ! empty( $settings['show_swatches'] ) ) {
418 347 $visibility[] = 'swatches';
419 348 }
420 349
@@ -480,23 +409,13 @@
480 409 * @return int
481 410 */
482 411 public static function default_columns( $layout ) {
483 412 switch ( $layout ) {
484 - case 'rtsb-post-grid-layout1':
485 - case 'rtsb-post-grid-layout2':
486 - case 'rtsb-post-grid-layout3':
487 - case 'rtsb-team-layout1':
488 - case 'rtsb-team-layout2':
489 - case 'rtsb-testimonial-layout2':
490 413 case 'grid-layout2':
491 414 case 'slider-layout2':
492 - case 'rtsb-coupon-layout1':
493 - case 'rtsb-coupon-layout2':
494 - case 'rtsb-coupon-layout3':
495 415 $columns = 3;
496 416 break;
497 - case 'rtsb-post-list-layout1':
498 - case 'rtsb-post-list-layout2':
417 +
499 418 case 'list-layout1':
500 419 case 'list-layout2':
501 420 case 'list-layout3':
502 421 case 'list-layout7':
@@ -502,13 +421,9 @@
502 421 case 'list-layout7':
503 422 case 'list-layout5':
504 423 $columns = 1;
505 424 break;
506 - case 'rtsb-team-layout4':
507 - case 'rtsb-testimonial-layout1':
508 - case 'rtsb-testimonial-layout3':
509 - case 'rtsb-testimonial-layout4':
510 - case 'rtsb-testimonial-layout5':
425 +
511 426 case 'list-layout4':
512 427 case 'list-layout6':
513 428 case 'slider-layout5':
514 429 case 'slider-layout8':
@@ -513,10 +428,9 @@
513 428 case 'slider-layout5':
514 429 case 'slider-layout8':
515 430 $columns = 2;
516 431 break;
517 - case 'rtsb-logo-layout1':
518 - case 'rtsb-logo-layout2':
432 +
519 433 case 'grid-layout6':
520 434 case 'grid-layout9':
521 435 $columns = 5;
522 436 break;
@@ -543,18 +457,9 @@
543 457 */
544 458 public static function pagination_data( $metas, $template ) {
545 459 $el_data = self::meta_dataset( $metas, $template );
546 460
547 - // Returned as an attribute *value*; the caller escapes it for the
548 - // attribute context (Elementor's Utils::render_html_attributes() applies
549 - // esc_attr()). esc_js() here only double-encoded the payload and, via
550 - // addslashes(), corrupted the JSON escapes for apostrophes and
551 - // non-ASCII search terms. The HTML-safe flags keep the payload free of
552 - // characters esc_attr() would have to encode - see self::archive_data().
553 - return wp_json_encode(
554 - $el_data,
555 - JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT
556 - );
461 + return esc_js( wp_json_encode( $el_data, JSON_UNESCAPED_UNICODE ) );
557 462 }
558 463
559 464 /**
560 465 * Container classes
@@ -576,9 +481,9 @@
576 481
577 482 $animation = $metas['hover_animation'];
578 483
579 484 if ( ! $is_cat ) {
580 - $cart_txt_class = $metas['show_cart_text'] ? ' show-cart-text' : ' no-cart-text';
485 + $cart_txt_class = $metas['show_cart_text'] ? ' has-cart-text' : ' no-cart-text';
581 486 $animation .= ' gallery-hover-' . ( ! empty( $settings['gallery_hover_animation'] ) ? $settings['gallery_hover_animation'] : 'fade' );
582 487 }
583 488
584 489 $badge_class = [
@@ -599,21 +504,14 @@
599 504
600 505 if ( in_array( 'single-cat', $metas['visibility'], true ) ) {
601 506 $classes .= ' show-single-cat';
602 507 }
603 - if ( in_array( 'single-brand', $metas['visibility'], true ) ) {
604 - $classes .= ' show-single-brand';
605 - }
606 508
607 509 if ( ! $is_cat ) {
608 510 $action_btn_classes = [
609 - 'show_compare_text' => 'show-compare-text',
610 - 'show_quick_view_text' => 'show-quick-view-text',
611 - 'show_wishlist_text' => 'show-wishlist-text',
612 - // Quick checkout only had its icon entry here, so the "Show Button Text?"
613 - // switch had nothing to emit and the label stayed hidden whatever it was
614 - // set to.
615 - 'show_quick_checkout_text' => 'show-quick-checkout-text',
511 + 'show_compare_text' => 'has-compare-text',
512 + 'show_quick_view_text' => 'has-quick-view-text',
513 + 'show_wishlist_text' => 'has-wishlist-text',
616 514 'show_compare_icon' => 'no-compare-icon',
617 515 'show_quick_view_icon' => 'no-quick-view-icon',
618 516 'show_wishlist_icon' => 'no-wishlist-icon',
619 517 'show_quick_checkout_icon' => 'no-quick_checkout-icon',
@@ -646,9 +544,9 @@
646 544 *
647 545 * @return string
648 546 */
649 547 public static function filter_layout( $layout, $template = '' ) {
650 - $allowed_patterns = apply_filters( 'rtsb/elementor/custom_layout', [ 'grid', 'list', 'slider', 'category', 'toyup', 'zilly', 'rtsb' ] );
548 + $allowed_patterns = apply_filters( 'rtsb/elementor/custom_layout', [ 'grid', 'list', 'slider', 'category', 'toyup', 'zilly' ] );
651 549 $layout_part = explode( '-', $layout );
652 550 $default = 'grid-layout1';
653 551
654 552 if ( empty( $layout_part[0] ) ) {
@@ -655,54 +553,31 @@
655 553 return $default;
656 554 }
657 555
658 556 if ( in_array( $layout_part[0], $allowed_patterns, true ) ) {
659 - if ( ! rtsb()->has_pro() && ! in_array( $layout, array_keys( Fns::free_layouts( $layout ) ), true ) ) {
660 - $layout = self::set_default_layout( $layout );
661 - }
662 -
663 - return self::resolve_existing_layout( $layout, $template );
557 + return $layout;
664 558 } else {
665 - return ! empty( $template ) ? self::resolve_existing_layout( self::set_default_layout( $layout ), $template ) : $default;
666 - }
667 - }
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 );
668 564
669 - /**
670 - * Falls back to the default layout when the resolved template file cannot
671 - * be found.
672 - *
673 - * Layouts contributed by a theme or an addon (rtsb-*, toyup-*, zilly-*)
674 - * stay saved in the Elementor document after that theme or addon is gone.
675 - * Without this check the loop asks for a template that no longer exists,
676 - * so nothing renders and load_template() raises a "does not exist" notice
677 - * for every product in the loop.
678 - *
679 - * @param string $layout Resolved layout name.
680 - * @param string $template Template directory, e.g. "elementor/general/grid/".
681 - * Empty when the layout is only used for CSS classes.
682 - *
683 - * @return string
684 - */
685 - private static function resolve_existing_layout( $layout, $template ) {
686 - if ( empty( $template ) || empty( $layout ) ) {
687 - return $layout;
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 + }
688 579 }
689 -
690 - $cache_key = 'layout_exists_' . $template . $layout;
691 -
692 - if ( isset( self::$cache[ $cache_key ] ) ) {
693 - return self::$cache[ $cache_key ];
694 - }
695 -
696 - $resolved = $layout;
697 -
698 - if ( ! Fns::locate_template( $template . $layout ) ) {
699 - $resolved = self::set_default_layout( $layout );
700 - }
701 -
702 - self::$cache[ $cache_key ] = $resolved;
703 -
704 - return $resolved;
705 580 }
706 581
707 582 /**
708 583 * Row classes
@@ -713,13 +588,10 @@
713 588 */
714 589 public static function prepare_row_classes( $settings = [] ) {
715 590 $masonry_class = null;
716 591 $classes = 'rtsb-row rtsb-content-loader element-loading';
592 + $loader_class = ' rtsb-pre-loader';
717 593
718 - $loader_class = '';
719 - if ( Fns::enable_loader() ) {
720 - $loader_class = ' rtsb-pre-loader';
721 - }
722 594 $raw_layout = esc_html( $settings['layout'] );
723 595 $layout = self::filter_layout( $raw_layout );
724 596 $grid_type = $settings['grid_type'];
725 597 $is_carousel = preg_match( '/slider/', $layout );
@@ -727,14 +599,12 @@
727 599 if ( preg_match( '/category/', $layout ) && ! empty( $settings['active_cat_slider'] ) ) {
728 600 $classes .= ' rtsb-slider-layout';
729 601 }
730 602
731 - if ( 'equal' === $grid_type ) {
732 - $masonry_class = ' rtsb-equal';
603 + if ( 'even' === $grid_type ) {
604 + $masonry_class = ' rtsb-even';
733 605 } elseif ( 'masonry' === $grid_type ) {
734 606 $masonry_class = ' rtsb-masonry';
735 - } else {
736 - $masonry_class = ' rtsb-even';
737 607 }
738 608
739 609 if ( ! rtsb()->has_pro() || $is_carousel ) {
740 610 $masonry_class = ' rtsb-even';
@@ -746,8 +616,9 @@
746 616
747 617 if ( ! empty( $settings['raw_settings']['inner_slider_always_show_nav'] ) ) {
748 618 $classes .= ' inner-slider-always-show-nav';
749 619 }
620 +
750 621 if ( empty( $settings['raw_settings']['cols_mobile'] ) ) {
751 622 $classes .= ' rtsb-mobile-flex-row';
752 623 }
753 624
@@ -775,12 +646,11 @@
775 646 return $has_filters ? self::pagination_data( $settings, $template ) : '';
776 647 } elseif ( $is_cat ) {
777 648 return '';
778 649 } else {
779 - $ajax_pagination = ! empty( $settings['show_pagination'] ) && 'pagination' !== $settings['pagination_type'];
780 - $page = Fns::product_filters_has_ajax( apply_filters( 'rtsb/builder/set/current/page/type', '' ) );
781 - $is_shop_or_archive = is_shop() || is_product_taxonomy();
782 - $condition = rtsb()->has_pro() && $ajax && ( $ajax_pagination || $has_filters || $page || $is_shop_or_archive );
650 + $ajax_pagination = ! empty( $settings['show_pagination'] ) && 'pagination' !== $settings['pagination_type'];
651 + $page = Fns::product_filters_has_ajax( apply_filters( 'rtsb/builder/set/current/page/type', '' ) );
652 + $condition = rtsb()->has_pro() && $ajax && ( $ajax_pagination || $has_filters || $page );
783 653
784 654 return $condition ? self::pagination_data( $settings, $template ) : '';
785 655 }
786 656 }
@@ -795,23 +665,9 @@
795 665 */
796 666 public static function archive_data( $metas, $template ) {
797 667 $el_data = self::archive_meta_dataset( $metas, $template );
798 668
799 - // The return value is an HTML attribute, so it must be escaped for the
800 - // HTML attribute context. esc_js() is a JavaScript-string escaper: it
801 - // turns ' back into a bare apostrophe and relies on a backslash
802 - // that means nothing to an HTML parser, which let a search term close
803 - // the attribute and inject its own. esc_attr() encodes both quote
804 - // characters, so the value can never terminate the attribute.
805 - //
806 - // The JSON is additionally encoded with the HTML-safe flags so it holds
807 - // no <, >, &, ' or " at all. That keeps a search term that itself looks
808 - // like an entity (e.g. &quot;) intact: esc_attr() does not double-encode
809 - // an existing entity, so without this the browser would decode the
810 - // term back into a raw quote and break JSON.parse().
811 - $json = wp_json_encode( $el_data, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT );
812 -
813 - return sprintf( ' data-rtsb-ajax="%s"', esc_attr( $json ) );
669 + return ' data-rtsb-ajax=\'' . esc_js( wp_json_encode( $el_data ) ) . '\'';
814 670 }
815 671
816 672 /**
817 673 * Slider options.
@@ -828,9 +684,9 @@
828 684 $d_col = 0 === $meta['d_cols'] ? self::default_columns( $meta['layout'] ) : $meta['d_cols'];
829 685 $t_col = 0 === $meta['t_cols'] ? 2 : $meta['t_cols'];
830 686 $m_col = 0 === $meta['m_cols'] ? 1 : $meta['m_cols'];
831 687
832 - if ( Plugin::$instance->breakpoints->has_custom_breakpoints() ) {
688 + if ( \Elementor\Plugin::$instance->breakpoints->has_custom_breakpoints() ) {
833 689 // Widescreen.
834 690 $w_col = self::get_data( $settings, 'cols_widescreen' );
835 691 $w_col = 0 === $w_col ? self::default_columns( $meta['layout'] ) : $w_col;
836 692 $w_group = self::get_data( $settings, 'cols_group_widescreen', 1 );
@@ -861,11 +717,8 @@
861 717 $breakpoints = [
862 718 0 => [
863 719 'slidesPerView' => absint( $m_col ),
864 720 'slidesPerGroup' => absint( $meta['m_group'] ),
865 - 'grid' => [
866 - 'rows' => 1,
867 - ],
868 721 'pagination' => [
869 722 'dynamicBullets' => $has_dynamic_dots,
870 723 ],
871 724 ],
@@ -1041,11 +894,9 @@
1041 894
1042 895 if ( $is_carousel ) {
1043 896 $loader_class = ' full-op';
1044 897 }
1045 - if ( ! Fns::enable_loader() ) {
1046 - return '';
1047 - }
898 +
1048 899 return '<div class="rtsb-elements-loading rtsb-ball-clip-rotate"><div></div></div>';
1049 900 }
1050 901
1051 902 /**
@@ -1086,11 +937,10 @@
1086 937 *
1087 938 * @return void
1088 939 */
1089 940 public static function register_scripts( $scripts ) {
1090 - $caro = false;
1091 - $masonry = false;
1092 - $script = [];
941 + $caro = false;
942 + $script = [];
1093 943
1094 944 $script[] = 'jquery';
1095 945
1096 946 foreach ( $scripts as $sc => $value ) {
@@ -1097,12 +947,8 @@
1097 947 if ( ! empty( $sc ) ) {
1098 948 if ( 'isCarousel' === $sc ) {
1099 949 $caro = $value;
1100 950 }
1101 -
1102 - if ( 'isMasonry' === $sc ) {
1103 - $masonry = $value;
1104 - }
1105 951 }
1106 952 }
1107 953
1108 954 if ( count( $scripts ) ) {
@@ -1112,15 +958,11 @@
1112 958 if ( $caro ) {
1113 959 $script[] = 'swiper';
1114 960 }
1115 961
1116 - if ( $masonry ) {
1117 - $script[] = 'masonry';
1118 - }
962 + $script[] = 'rtsb-imagesloaded';
963 + $script[] = 'rtsb-public';
1119 964
1120 - $script[] = 'imagesloaded';
1121 - $script[] = Fns::optimized_handle( 'rtsb-public' );
1122 -
1123 965 foreach ( $script as $sc ) {
1124 966 wp_enqueue_script( $sc );
1125 967 }
1126 968 }
@@ -1162,983 +1004,6 @@
1162 1004 $products_col = absint( get_option( 'woocommerce_catalog_columns', 4 ) );
1163 1005 $products_per_page = apply_filters( 'rtsb/elementor/archive/products_per_page', $products_row * $products_col );
1164 1006
1165 1007 return ! empty( $products_per_page ) ? $products_per_page : 12;
1166 - }
1167 - /**
1168 - * Render Filters View.
1169 - *
1170 - * @param array $templates Template name.
1171 - * @param array $settings Control settings.
1172 - *
1173 - * @return string
1174 - */
1175 - public static function filters_view( $templates, $settings ) {
1176 - global $wp;
1177 - $filters = [];
1178 - $html = null;
1179 - $reset = ! empty( $settings['reset_btn'] );
1180 - $reset_mode = 'show' === $settings['reset_btn_behavior'] ? ' show-reset' : ' ondemand-reset';
1181 - $reset_text = ! empty( $settings['reset_btn_text'] ) ? $settings['reset_btn_text'] : esc_html__( 'Reset Filter', 'shopbuilder' );
1182 - $scroll_mode = ! empty( $settings['enable_scroll'] ) ? ' has-scroll' : ' no-scroll';
1183 - $scroll_height = ! empty( $settings['enable_scroll'] ) ? $settings['scroll_height']['size'] : 300;
1184 - $has_mobile_toggle = ! empty( $settings['filter_mobile_toggle'] );
1185 - $toggle_class = $has_mobile_toggle ? ' default-filter-has-toggle' : ' default-filter-no-toggle';
1186 - $mobile_toggle_text = ! empty( $settings['filter_mobile_toggle_text'] ) ? $settings['filter_mobile_toggle_text'] : esc_html__( 'Click to Filter', 'shopbuilder' );
1187 - $scroll_attr = '';
1188 -
1189 - if ( ! empty( $settings['enable_scroll'] ) ) {
1190 - $scroll_attr = ' style="--rtsb-filter-scroll-height: ' . absint( $scroll_height ) . 'px;"';
1191 - }
1192 -
1193 - foreach ( $settings['filter_types'] as $key => $item ) {
1194 - if ( ! defined( 'RTWPVS_PLUGIN_FILE' ) && in_array( $item['input_type_all'], [ 'color', 'button', 'image' ], true ) ) {
1195 - $item['input_type_all'] = 'checkbox';
1196 - }
1197 - $filters[] = [
1198 - 'filter_title' => $item['filter_title'],
1199 - 'filter_items' => $item['filter_items'],
1200 - 'filter_attr' => $item['filter_attr'],
1201 - 'input_type' => 'product_attr' === $item['filter_items'] ? $item['input_type_all'] : $item['input_type'],
1202 - ];
1203 -
1204 - if ( 'rating_filter' === $item['filter_items'] ) {
1205 - $filters[ $key ]['template'] = $templates['rating'];
1206 - } elseif ( 'price_filter' === $item['filter_items'] ) {
1207 -
1208 - $filters[ $key ]['input_type'] = 'slider';
1209 - $filters[ $key ]['template'] = $templates['price'];
1210 - } else {
1211 - $filters[ $key ]['template'] = 'product_attr' === $item['filter_items'] ? $templates[ $item['input_type_all'] ] : $templates[ $item['input_type'] ];
1212 - }
1213 - }
1214 -
1215 - if ( '' === get_option( 'permalink_structure' ) ) {
1216 - $form_action = remove_query_arg( [ 'page', 'paged', 'product-page' ], add_query_arg( $wp->query_string, '', home_url( $wp->request ) ) );
1217 - } else {
1218 - $form_action = preg_replace( '%\/page/[0-9]+%', '', home_url( trailingslashit( $wp->request ) ) );
1219 - }
1220 - $args = [
1221 - 'filters' => $filters,
1222 - 'scroll_mode' => $scroll_mode,
1223 - 'reset_mode' => $reset_mode,
1224 - 'reset' => $reset,
1225 - 'scroll_attr' => $scroll_attr,
1226 - 'toggle_class' => $toggle_class,
1227 - 'reset_text' => $reset_text,
1228 - 'settings' => $settings,
1229 - 'form_action' => $form_action,
1230 - ];
1231 -
1232 - $html .= '<div class="rtsb-default-archive-filters' . esc_attr( $toggle_class ) . '">';
1233 -
1234 - if ( $has_mobile_toggle ) {
1235 - $html .= '<div class="rtsb-filter-mobile-toggle">
1236 - <button class="product-filter-toggle">
1237 - <span class="icon"><i aria-hidden="true" class="toggle-icon rtsb-icon rtsb-icon-filter"></i></span>
1238 - <span class="text reset-text">' . esc_html( $mobile_toggle_text ) . '</span>
1239 - <span></span>
1240 - </button>
1241 - </div>';
1242 - }
1243 -
1244 - $html .= Fns::load_template( $templates['layout'], $args, true );
1245 - $html .= '</div>';
1246 - return $html;
1247 - }
1248 - /**
1249 - * Get taxonomy type.
1250 - *
1251 - * @param string $tax_type Taxonomy type.
1252 - * @param string $attr_type Attribute type.
1253 - *
1254 - * @return string
1255 - */
1256 - public static function get_product_filters_tax_type( $tax_type, $attr_type = '' ) {
1257 - if ( 'product_attr' !== $tax_type ) {
1258 - return $tax_type;
1259 - }
1260 -
1261 - if ( empty( $attr_type ) ) {
1262 - return $tax_type;
1263 - }
1264 -
1265 - return $attr_type;
1266 - }
1267 - /**
1268 - * Get attribute terms.
1269 - *
1270 - * @param string $tax Taxonomy type.
1271 - * @return int[]|string|string[]|\WP_Error|\WP_Term[]
1272 - */
1273 - public static function get_product_filters_attribute_terms( $tax ) {
1274 - $cache_key = 'get_default_filter_attribute_terms_' . $tax;
1275 - if ( isset( self::$cache[ $cache_key ] ) ) {
1276 - return self::$cache[ $cache_key ];
1277 - }
1278 -
1279 - $attributes = wc_get_attribute_taxonomies();
1280 - $att_name = str_replace( 'pa_', '', $tax );
1281 - $exist = array_search( $att_name, array_column( $attributes, 'attribute_name' ), true );
1282 - if ( false === $exist ) {
1283 - self::$cache[ $cache_key ] = [];
1284 - return self::$cache[ $cache_key ];
1285 - }
1286 - $attribute = $tax;
1287 -
1288 - if ( ! is_shop() && is_product_taxonomy() ) {
1289 - $term = get_queried_object();
1290 -
1291 - if ( ! $term instanceof WP_Term ) {
1292 - return [];
1293 - }
1294 -
1295 - $args = [
1296 - 'status' => 'publish',
1297 - 'limit' => -1,
1298 - 'return' => 'ids',
1299 - ];
1300 -
1301 - if ( 'product_cat' === $term->taxonomy ) {
1302 - $args['category'] = [ $term->slug ];
1303 - }
1304 -
1305 - if ( 'product_tag' === $term->taxonomy ) {
1306 - $args['tag'] = [ $term->slug ];
1307 - }
1308 -
1309 - if ( strpos( $term->taxonomy, 'pa_' ) !== false ) {
1310 - $args['tax_query'] = [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
1311 - [
1312 - 'taxonomy' => $term->taxonomy,
1313 - 'field' => 'term_id',
1314 - 'terms' => $term->term_id,
1315 - 'operator' => 'IN',
1316 - ],
1317 - ];
1318 - }
1319 -
1320 - $query = new WC_Product_Query( $args );
1321 - $products = $query->get_products();
1322 - $attr_terms = [];
1323 -
1324 - foreach ( $products as $product_id ) {
1325 - $product_attr = wp_get_post_terms( $product_id, $attribute );
1326 -
1327 - foreach ( $product_attr as $attr ) {
1328 - $attr_terms[] = $attr;
1329 - }
1330 - }
1331 - self::$cache[ $cache_key ] = self::filter_products_array_unique( $attr_terms );
1332 - return self::$cache[ $cache_key ];
1333 - } else {
1334 - self::$cache[ $cache_key ] = get_terms(
1335 - [
1336 - 'taxonomy' => $attribute,
1337 - 'hide_empty' => false,
1338 - ]
1339 - );
1340 - return self::$cache[ $cache_key ];
1341 - }
1342 - }
1343 - /**
1344 - * Remove duplicate values from an array.
1345 - *
1346 - * @param array $array The input array.
1347 - * @param bool $keep_key_assoc Whether to keep the key associations after removing duplicates.
1348 - * @return array The array with duplicates removed.
1349 - */
1350 - public static function filter_products_array_unique( $array, $keep_key_assoc = false ) {
1351 - $duplicate_keys = [];
1352 - $tmp = [];
1353 -
1354 - foreach ( $array as $key => $val ) {
1355 - // convert objects to arrays, in_array() does not support objects.
1356 - if ( is_object( $val ) ) {
1357 - $val = (array) $val;
1358 - }
1359 -
1360 - if ( ! in_array( $val, $tmp, true ) ) {
1361 - $tmp[] = $val;
1362 - } else {
1363 - $duplicate_keys[] = $key;
1364 - }
1365 - }
1366 -
1367 - foreach ( $duplicate_keys as $key ) {
1368 - unset( $array[ $key ] );
1369 - }
1370 -
1371 - return $keep_key_assoc ? $array : array_values( $array );
1372 - }
1373 - /**
1374 - * Retrieve product categories (including hierarchical support)
1375 - *
1376 - * @param string $taxonomy Taxonomy name.
1377 - * @return int[]|string|string[]|\WP_Error|\WP_Term[]
1378 - */
1379 - public static function get_all_terms( $taxonomy ) {
1380 - $args = [
1381 - 'taxonomy' => $taxonomy,
1382 - 'suppress_filter' => true,
1383 - 'hide_empty' => false,
1384 - 'pad_counts' => true,
1385 - 'hierarchical' => true,
1386 - ];
1387 -
1388 - // Archive-scope only when the URL path itself is a term archive
1389 - // (e.g. /product-category/clothing/). On the shop page WordPress can
1390 - // resolve ?product_cat=X to a queried term, but in that case we want
1391 - // the full hierarchy to remain visible for additional filtering.
1392 - $archive_term = self::request_path_is_term_archive( $taxonomy ) ? self::resolve_archive_term_from_path( $taxonomy ) : null;
1393 -
1394 - // The cache key MUST include the archive term id. The scoped list is
1395 - // different for every term archive, so a taxonomy-only key leaks the
1396 - // first visited category's term list into every other category archive
1397 - // when a persistent object cache is active - the leaked terms are then
1398 - // counted as empty in the current archive and the whole filter renders
1399 - // blank.
1400 - $cache_key = $archive_term instanceof WP_Term
1401 - ? 'rtsb_get_terms_filter_products_archive_' . $taxonomy . '_' . $archive_term->term_id
1402 - : 'rtsb_get_terms_filter_products_full_' . $taxonomy;
1403 -
1404 - $taxonomy_terms = wp_cache_get( $cache_key, Cache::group() );
1405 -
1406 - if ( false === $taxonomy_terms ) {
1407 - if ( $archive_term instanceof WP_Term ) {
1408 - // Treat the URL-derived term as the root of the visible
1409 - // hierarchy so the recursive renderer in
1410 - // get_product_default_filter_list_html() (which starts
1411 - // with $parent = 0) picks it up on child-category
1412 - // archives. Clone first to avoid mutating WP's term
1413 - // object.
1414 - $current_term = clone $archive_term;
1415 - $current_term->parent = 0;
1416 - $taxonomy_terms = [ $current_term ];
1417 - $child_args = [
1418 - 'taxonomy' => $taxonomy,
1419 - 'child_of' => $archive_term->term_id,
1420 - 'hide_empty' => false,
1421 - ];
1422 -
1423 - $child_terms = get_terms( $child_args );
1424 -
1425 - if ( ! empty( $child_terms ) && ! is_wp_error( $child_terms ) ) {
1426 - $taxonomy_terms = array_merge( $taxonomy_terms, $child_terms );
1427 - }
1428 - } else {
1429 - $taxonomy_terms = get_terms( $args );
1430 - }
1431 -
1432 - wp_cache_set( $cache_key, $taxonomy_terms, Cache::group(), 12 * HOUR_IN_SECONDS );
1433 - Cache::set_data_cache_key( $cache_key );
1434 - }
1435 -
1436 - return $taxonomy_terms;
1437 - }
1438 -
1439 - /**
1440 - * Resolve the archive term from the request URL path. Avoids relying on
1441 - * get_queried_object() which can return an unexpected term when
1442 - * ?product_cat=a,b,c is also present in the request.
1443 - *
1444 - * @param string $taxonomy Taxonomy slug.
1445 - *
1446 - * @return \WP_Term|null
1447 - */
1448 - private static function resolve_archive_term_from_path( $taxonomy ) {
1449 - $rewrite_slugs = self::get_taxonomy_archive_slugs( $taxonomy );
1450 -
1451 - if ( empty( $rewrite_slugs ) ) {
1452 - return null;
1453 - }
1454 -
1455 - // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Path only used for slug extraction.
1456 - $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '';
1457 - // phpcs:enable
1458 - $path = (string) wp_parse_url( $request_uri, PHP_URL_PATH );
1459 -
1460 - if ( '' === $path ) {
1461 - return null;
1462 - }
1463 -
1464 - foreach ( $rewrite_slugs as $base ) {
1465 - $base = trim( $base, '/' );
1466 - $needle = '/' . $base . '/';
1467 -
1468 - $pos = strpos( $path, $needle );
1469 - if ( false === $pos ) {
1470 - continue;
1471 - }
1472 -
1473 - $remainder = substr( $path, $pos + strlen( $needle ) );
1474 - $segments = array_values(
1475 - array_filter(
1476 - explode( '/', $remainder ),
1477 - static function ( $segment ) {
1478 - return '' !== $segment;
1479 - }
1480 - )
1481 - );
1482 -
1483 - if ( empty( $segments ) ) {
1484 - continue;
1485 - }
1486 -
1487 - $slug = sanitize_title( end( $segments ) );
1488 - $term = get_term_by( 'slug', $slug, $taxonomy );
1489 -
1490 - if ( $term instanceof WP_Term ) {
1491 - return $term;
1492 - }
1493 - }
1494 -
1495 - return null;
1496 - }
1497 -
1498 - /**
1499 - * Decide whether the current REQUEST_URI path represents a real term
1500 - * archive page for the given taxonomy. URL-based so it ignores cases
1501 - * where WordPress merely resolves a query string into a queried term
1502 - * on the shop page.
1503 - *
1504 - * @param string $taxonomy Taxonomy slug.
1505 - *
1506 - * @return bool
1507 - */
1508 - private static function request_path_is_term_archive( $taxonomy ) {
1509 - $rewrite_slugs = self::get_taxonomy_archive_slugs( $taxonomy );
1510 -
1511 - if ( empty( $rewrite_slugs ) ) {
1512 - return false;
1513 - }
1514 -
1515 - // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Path only used in match.
1516 - $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '';
1517 - // phpcs:enable
1518 - $path = (string) wp_parse_url( $request_uri, PHP_URL_PATH );
1519 -
1520 - if ( '' === $path ) {
1521 - return false;
1522 - }
1523 -
1524 - foreach ( $rewrite_slugs as $slug ) {
1525 - if ( '' === $slug ) {
1526 - continue;
1527 - }
1528 - if ( false !== strpos( $path, '/' . trim( $slug, '/' ) . '/' ) ) {
1529 - return true;
1530 - }
1531 - }
1532 -
1533 - return false;
1534 - }
1535 -
1536 - /**
1537 - * Resolve the URL slug(s) used by a taxonomy archive (e.g. product_cat
1538 - * maps to "product-category"). Falls back to common WooCommerce defaults
1539 - * when the taxonomy object is unavailable.
1540 - *
1541 - * @param string $taxonomy Taxonomy slug.
1542 - *
1543 - * @return array<string>
1544 - */
1545 - private static function get_taxonomy_archive_slugs( $taxonomy ) {
1546 - $slugs = [];
1547 -
1548 - if ( taxonomy_exists( $taxonomy ) ) {
1549 - $tax_obj = get_taxonomy( $taxonomy );
1550 - if ( $tax_obj && ! empty( $tax_obj->rewrite['slug'] ) ) {
1551 - $slugs[] = $tax_obj->rewrite['slug'];
1552 - }
1553 - }
1554 -
1555 - $defaults = [
1556 - 'product_cat' => 'product-category',
1557 - 'product_tag' => 'product-tag',
1558 - 'product_brand' => 'product-brand',
1559 - ];
1560 -
1561 - if ( isset( $defaults[ $taxonomy ] ) ) {
1562 - $slugs[] = $defaults[ $taxonomy ];
1563 - }
1564 -
1565 - return array_unique( array_filter( $slugs ) );
1566 - }
1567 - /**
1568 - * Generates an error message block.
1569 - *
1570 - * @param array $title The title array of the error message block.
1571 - * @param string $wrapper Additional CSS class for the wrapper div.
1572 - *
1573 - * @return string The HTML representation of the error message block.
1574 - */
1575 - public static function filter_error_message( $title, $wrapper ) {
1576 - $html = '<div class="rtsb-product-default-filters ' . esc_attr( $wrapper ) . '">';
1577 - $html .= self::get_default_filter_title( $title );
1578 - $html .= '<div class="default-filter-content">';
1579 - $html .= '<p class="no-filter">' . esc_html__( 'Nothing found.', 'shopbuilder' ) . '</p>';
1580 - $html .= '</div>';
1581 - $html .= '</div>';
1582 -
1583 - return $html;
1584 - }
1585 - /**
1586 - * Get filter title.
1587 - *
1588 - * @param array $title Filter title.
1589 - * @return string
1590 - */
1591 - public static function get_default_filter_title( $title ) {
1592 - $html = '';
1593 -
1594 - if ( empty( $title['title'] ) ) {
1595 - return $html;
1596 - }
1597 -
1598 - $html .= '<div class="default-filter-title-wrapper">';
1599 -
1600 - $html .= '<h3 class="widget-title rtsb-d-flex rtsb-align-items-center"><span class="title">' . $title['title'] . '</span></h3>';
1601 -
1602 - $html .= '</div>';
1603 -
1604 - return apply_filters( 'rtsb/elementor/default/filter/title', $html, $title );
1605 - }
1606 - /**
1607 - * Recursive function to generate the filter list with hierarchy.
1608 - *
1609 - * @param array $data Array of required data.
1610 - * @param string $input Type of input field for the filter (e.g., checkbox).
1611 - * @param array $additional_data Data for additional filtering.
1612 - * @param int $parent ID of the parent term (default: 0).
1613 - *
1614 - * @return string The generated HTML for the product filter list.
1615 - */
1616 - public static function get_product_default_filter_list_html( $data, $input = 'checkbox', $additional_data = [], $parent = 0 ) {
1617 -
1618 - $html = '';
1619 - $default_tax = [];
1620 - $is_attribute = ! empty( $data['is_attribute'] );
1621 -
1622 - $param_key = isset( $additional_data['taxonomy'] ) ? $additional_data['taxonomy'] : '';
1623 - if ( $param_key && 0 === strpos( $param_key, 'pa_' ) ) {
1624 - $param_key = 'filter_' . substr( $param_key, 3 );
1625 - }
1626 -
1627 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1628 - $raw_values = ( $param_key && isset( $_GET[ $param_key ] ) ) ? wc_clean( wp_unslash( $_GET[ $param_key ] ) ) : '';
1629 - $active_option = $raw_values ? array_map( 'urldecode', explode( ',', $raw_values ) ) : [];
1630 - $counter = 0;
1631 -
1632 - if ( $is_attribute ) {
1633 - list(
1634 - 'filter_name' => $filter_name,
1635 - 'base_link' => $base_link
1636 - ) = self::get_product_filters_attribute_term_info( $additional_data['taxonomy'] );
1637 - }
1638 -
1639 - $html .= '<ul class="product-default-filters input-type-' . esc_attr( $input ) . '">';
1640 -
1641 - foreach ( $data['taxonomy'] as $tax ) {
1642 - if ( $tax->parent !== $parent ) {
1643 - continue;
1644 - }
1645 -
1646 - if ( $is_attribute ) {
1647 - $tax_link = remove_query_arg( $filter_name, $base_link );
1648 - $tax_link = add_query_arg( $filter_name, $tax->slug, $tax_link );
1649 - } else {
1650 - $tax_link = get_term_link( $tax );
1651 - }
1652 -
1653 - $unique_id = substr( uniqid(), -4 );
1654 -
1655 - $decoded_slug = urldecode( $tax->slug );
1656 - $is_active = in_array( $decoded_slug, $active_option, true ) || in_array( $tax->slug, $active_option, true );
1657 - $active_tax = $is_active ? ' checked' : '';
1658 - $active_tax_parent = $is_active ? ' active' : '';
1659 - $term_children = in_array( $tax->taxonomy, [ 'product_cat', 'product_brand' ], true ) ? get_term_children( $tax->term_id, $tax->taxonomy ) : [];
1660 - $taxonomy = $tax->taxonomy;
1661 - $product_count = self::get_visible_term_count( $tax->taxonomy, $tax->term_id );
1662 -
1663 - // On a product taxonomy archive (category/tag/attribute page) scope the
1664 - // count to the current archive - only products that are in THIS archive
1665 - // AND have this term. This makes the displayed count reflect the current
1666 - // category context and lets terms that are empty in this archive be
1667 - // dropped from the list.
1668 - if ( is_product_taxonomy() || 'archive' === apply_filters( 'rtsb/builder/set/current/page/type', '' ) ) {
1669 - $product_count = self::count_tax_data( $tax, $product_count );
1670 - }
1671 -
1672 - // Show Empty Categories.
1673 - if ( empty( $data['show_empty_terms'] ) && 0 === $product_count ) {
1674 - continue;
1675 - }
1676 -
1677 - $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' : '' ) . '">
1678 - <div class="rtsb-default-filter-group' . esc_attr( $active_tax_parent ) . '">
1679 - <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 ) . '>
1680 - <label class="rtsb-default-' . esc_attr( $input ) . '-filter-label" for="rtsb-default-term-filter-' . $unique_id . absint( $tax->term_id ) . '">
1681 - <span class="name">' . esc_html( $tax->name ) . '</span>
1682 - </label>
1683 - <div class="rtsb-product-count">(' . esc_html( $product_count ) . ')</div>
1684 - ' . ( ! empty( $term_children ) && $data['show_child'] ? '<i class="rtsb-plus-icon"></i>' : '' ) . '
1685 - </div>';
1686 -
1687 - // Show Sub-categories.
1688 - if ( $data['show_child'] ) {
1689 - $children = self::get_product_default_filter_list_html( $data, $input, $additional_data, $tax->term_id );
1690 -
1691 - if ( ! empty( $children ) ) {
1692 - $html .= '<div class="filter-child">' . $children . '</div>';
1693 - }
1694 - }
1695 -
1696 - $counter++;
1697 -
1698 - $html .= '</li>';
1699 - }
1700 -
1701 - $html .= '</ul>';
1702 -
1703 - // Check if the output contains any child elements.
1704 - $has_children = strpos( $html, '<li class="rtsb-default-filter-term-item' ) !== false;
1705 -
1706 - if ( ! $has_children ) {
1707 - // Remove the outer <ul> if there are no child elements.
1708 - $html = '';
1709 - }
1710 - return $html;
1711 - }
1712 - /**
1713 - * Get attribute term info.
1714 - *
1715 - * @param string $tax Taxonomy type.
1716 - * @return array
1717 - */
1718 - public static function get_product_filters_attribute_term_info( $tax ) {
1719 - $result = [];
1720 - $attributes = wc_get_attribute_taxonomies();
1721 -
1722 - foreach ( $attributes as $attr ) {
1723 - if ( str_replace( 'pa_', '', $tax ) === $attr->attribute_name ) {
1724 - $term_type = $attr->attribute_type;
1725 - $attribute_slug = $attr->attribute_name;
1726 - $attribute = wc_attribute_taxonomy_name( $attr->attribute_name );
1727 - break;
1728 - }
1729 - }
1730 -
1731 - $filter_name = 'filter_' . wc_attribute_taxonomy_slug( $attribute_slug );
1732 -
1733 - $result['attribute'] = $attribute ?? '';
1734 - $result['term_type'] = $term_type ?? '';
1735 - $result['attribute_slug'] = $attribute_slug ?? '';
1736 - $result['filter_name'] = $filter_name;
1737 - $result['base_link'] = self::get_base_url();
1738 -
1739 - return $result;
1740 - }
1741 - /**
1742 - * Get base URL.
1743 - *
1744 - * @return string|null
1745 - */
1746 - public static function get_base_url() {
1747 - global $wp;
1748 - return home_url( add_query_arg( [], $wp->request ) );
1749 - }
1750 - /**
1751 - * Count the number of occurrences for a specific term in a taxonomy for the current queried object.
1752 - *
1753 - * @param object|array $tax The term object for the taxonomy.
1754 - * @param int $count The initial count value.
1755 - *
1756 - * @return int The updated count value.
1757 - */
1758 - public static function count_tax_data( $tax, $count ) {
1759 - $page = get_queried_object();
1760 -
1761 - // Scoping only applies on a real taxonomy archive where the queried
1762 - // object is a term. On other contexts (e.g. the shop page) the queried
1763 - // object is not a term, so keep the original (global) count.
1764 - if ( ! $page instanceof \WP_Term ) {
1765 - return $count;
1766 - }
1767 -
1768 - if ( is_array( $tax ) && strpos( $tax['taxonomy'], 'attribute_' ) !== false ) {
1769 - $taxonomy = str_replace( 'attribute_', '', $tax['taxonomy'] );
1770 - $term_id = $tax['term_id'];
1771 - } else {
1772 - $taxonomy = $tax->taxonomy;
1773 - $term_id = $tax->term_id;
1774 - }
1775 -
1776 - // A taxonomy selected through the query string is a removable choice,
1777 - // not a fixed archive context, and the filter ORs terms within one
1778 - // taxonomy. Scoping a term's count to that selection therefore
1779 - // advertises a result the filter never produces - on
1780 - // /shop/?product_cat=aliens the Movies count read 32 while choosing
1781 - // Movies returns 279 - and it hides every sibling term whose overlap
1782 - // with the selection is zero. Only a real term-archive URL
1783 - // (/product-category/aliens/) is a fixed context worth scoping to.
1784 - if ( $taxonomy === $page->taxonomy && ! self::request_path_is_term_archive( $page->taxonomy ) ) {
1785 - return $count;
1786 - }
1787 -
1788 - if ( strpos( $page->taxonomy, 'pa_' ) !== false ) {
1789 - $args['status'] = 'publish';
1790 - $args['limit'] = -1;
1791 - $args['return'] = 'ids';
1792 - $args['tax_query'] = [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
1793 - 'relation' => 'AND',
1794 - [
1795 - 'taxonomy' => $page->taxonomy,
1796 - 'field' => 'term_id',
1797 - 'terms' => $page->term_id,
1798 - 'operator' => 'IN',
1799 - ],
1800 - [
1801 - 'taxonomy' => $tax->taxonomy,
1802 - 'field' => 'term_id',
1803 - 'terms' => $tax->term_id,
1804 - 'operator' => 'IN',
1805 - ],
1806 - [
1807 - 'taxonomy' => 'product_visibility',
1808 - 'terms' => [ 'exclude-from-catalog' ],
1809 - 'field' => 'name',
1810 - 'operator' => 'NOT IN',
1811 - ],
1812 - ];
1813 -
1814 - return Fns::count_products_by_tax_query( $args['tax_query'] );
1815 - }
1816 -
1817 - // Scope the count to the current archive term (category, brand, tag, etc.)
1818 - // so it reflects only products that are in this archive AND also have the
1819 - // filter term. Works for cross-taxonomy filters (e.g. brands on a tag
1820 - // archive) and same-taxonomy filters (e.g. the Tags filter on a tag
1821 - // archive shows the intersection with the current tag).
1822 - $args = [
1823 - 'limit' => -1,
1824 - 'return' => 'ids',
1825 - 'status' => 'publish',
1826 - 'tax_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
1827 - 'relation' => 'AND',
1828 - [
1829 - 'taxonomy' => $taxonomy,
1830 - 'field' => 'term_id',
1831 - 'terms' => $term_id,
1832 - ],
1833 - [
1834 - 'taxonomy' => $page->taxonomy,
1835 - 'field' => 'term_id',
1836 - 'terms' => $page->term_id,
1837 - ],
1838 - [
1839 - 'taxonomy' => 'product_visibility',
1840 - 'terms' => [ 'exclude-from-catalog' ],
1841 - 'field' => 'name',
1842 - 'operator' => 'NOT IN',
1843 - ],
1844 - ],
1845 - ];
1846 -
1847 - return Fns::count_products_by_tax_query( $args['tax_query'] );
1848 - }
1849 -
1850 - /**
1851 - * Get the count of visible (non-hidden) products for a given taxonomy term.
1852 - *
1853 - * Excludes products with WooCommerce visibility set to 'Hidden'
1854 - * (products assigned the 'exclude-from-catalog' term).
1855 - *
1856 - * @param string $taxonomy The taxonomy name.
1857 - * @param int $term_id The term ID.
1858 - *
1859 - * @return int Visible product count.
1860 - */
1861 - public static function get_visible_term_count( $taxonomy, $term_id ) {
1862 - static $cache = [];
1863 -
1864 - $cache_key = $taxonomy . '_' . $term_id;
1865 -
1866 - if ( isset( $cache[ $cache_key ] ) ) {
1867 - return $cache[ $cache_key ];
1868 - }
1869 -
1870 - $args = [
1871 - 'limit' => -1,
1872 - 'return' => 'ids',
1873 - 'status' => 'publish',
1874 - 'tax_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
1875 - 'relation' => 'AND',
1876 - [
1877 - 'taxonomy' => $taxonomy,
1878 - 'field' => 'term_id',
1879 - 'terms' => $term_id,
1880 - ],
1881 - [
1882 - 'taxonomy' => 'product_visibility',
1883 - 'terms' => [ 'exclude-from-catalog' ],
1884 - 'field' => 'name',
1885 - 'operator' => 'NOT IN',
1886 - ],
1887 - ],
1888 - ];
1889 -
1890 - $count = Fns::count_products_by_tax_query( $args['tax_query'] );
1891 -
1892 - $cache[ $cache_key ] = $count;
1893 -
1894 - return $count;
1895 - }
1896 -
1897 - /**
1898 - * Get filter HTML.
1899 - *
1900 - * @param string $filter_type Filter type.
1901 - * @param array $options Options.
1902 - * @param array $additional_data Data for additional filtering.
1903 - * @param string $input Input type.
1904 - *
1905 - * @return string The generated HTML for the attribute filter list.
1906 - */
1907 - public static function get_default_filter_html( $filter_type, $options, $additional_data = [], $input = 'checkbox' ) {
1908 - $html = '<ul class="product-filters input-type-' . esc_attr( $input ) . '" ';
1909 - $active_option = isset( $_GET[ $filter_type ] ) ? explode( ',', wc_clean( wp_unslash( $_GET[ $filter_type ] ) ) ) : []; // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1910 - $option_link = self::get_base_url() . '/' . $filter_type . '/';
1911 -
1912 - foreach ( $options as $option => $option_name ) {
1913 - $unique_id = substr( uniqid(), -6 );
1914 - $active_tax = in_array( $option, $active_option, true ) ? ' checked' : '';
1915 - $active_group = in_array( $option, $active_option, true ) ? ' active' : '';
1916 -
1917 - $html .= '
1918 - <li class="rtsb-term-item">
1919 - <div class="rtsb-default-filter-group' . esc_attr( $active_group ) . '">
1920 - <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 . '>
1921 - <label class="rtsb-default-' . esc_attr( $input ) . '-filter-label" for="rtsb-term-default-filter-' . $unique_id . '">
1922 - <span class="name">' . esc_html( $option_name ) . '</span>
1923 - </label>
1924 - </div>
1925 - </li>';
1926 - }
1927 -
1928 - $html .= '</ul>';
1929 -
1930 - return $html;
1931 - }
1932 - /**
1933 - * Get the count of products with a specific star rating.
1934 - *
1935 - * @param int $star_rating The star rating value.
1936 - *
1937 - * @return int
1938 - */
1939 - public static function get_product_rating_count( $star_rating ) {
1940 - $args = [
1941 - 'status' => 'publish',
1942 - 'limit' => -1,
1943 - 'product_rating' => sprintf( '%.1f', $star_rating ),
1944 - ];
1945 -
1946 - if ( is_product_taxonomy() ) {
1947 - $term = get_queried_object();
1948 - $term_type = 'product_cat' === $term->taxonomy ? 'product_category_id' : 'product_tag_id';
1949 -
1950 - if ( $term instanceof WP_Term ) {
1951 - $args[ $term_type ] = [ $term->term_id ];
1952 - }
1953 - }
1954 -
1955 - $query = new WC_Product_Query( $args );
1956 - $products = $query->get_products();
1957 -
1958 - return count( $products );
1959 - }
1960 - /**
1961 - * Get attribute filter HTML.
1962 - *
1963 - * @param array $data Array of required data.
1964 - * @param string $input Type of input field for the filter (e.g., color).
1965 - *
1966 - * @return string The generated HTML for the attribute filter list.
1967 - */
1968 - public static function get_attribute_filter_list_html( $data, $input = 'color' ) {
1969 - if ( ! function_exists( 'rtwpvs' ) ) {
1970 - return '';
1971 - }
1972 -
1973 - $terms = $data['terms'];
1974 - $term_info = $data['term_info'];
1975 - $show_tooltips = $data['show_tooltips'];
1976 - $show_empty_terms = $data['show_empty_terms'];
1977 - $counter = 0;
1978 -
1979 - $html = '<ul class="product-default-filters rtsb-terms-wrapper ' . esc_attr( $term_info['type'] ) . '-variable-wrapper' . esc_attr( $data['show_label'] ) . '">';
1980 -
1981 - foreach ( $terms as $attr_term ) {
1982 - $count = self::get_visible_term_count( $attr_term->taxonomy, $attr_term->term_id );
1983 - $term_link = remove_query_arg( $term_info['filter_name'], $term_info['base_link'] );
1984 - $term_link = add_query_arg( $term_info['filter_name'], $attr_term->slug, $term_link );
1985 - $unique_id = substr( uniqid(), -6 );
1986 - $name = 'term-' . wc_variation_attribute_name( $term_info['attribute'] ) . '-' . $unique_id;
1987 - $decoded_slug = urldecode( $attr_term->slug );
1988 - $is_selected = in_array( $decoded_slug, $term_info['current_filter'], true ) || in_array( $attr_term->slug, $term_info['current_filter'], true );
1989 - $selected_class = $is_selected ? ' selected' : '';
1990 - $selected_item = $is_selected ? ' checked' : '';
1991 - $active_class = $is_selected ? ' active' : '';
1992 -
1993 - $args = [
1994 - 'id' => $name,
1995 - 'name' => $attr_term->name,
1996 - 'link' => $term_link,
1997 - 'type' => $term_info['type'],
1998 - 'term_id' => $attr_term->term_id,
1999 - 'term_slug' => $attr_term->slug,
2000 - 'taxonomy' => wc_variation_attribute_name( $term_info['attribute'] ),
2001 - 'tooltips' => $show_tooltips,
2002 - 'attribute_name' => $term_info['attribute'],
2003 - 'selected_item' => $selected_item,
2004 - ];
2005 -
2006 - if ( 'archive' === apply_filters( 'rtsb/builder/set/current/page/type', '' ) && ! empty( $data['count_display'] ) && 'block' === $data['count_display'] ) {
2007 - $count = self::count_tax_data( $args, $count );
2008 - }
2009 -
2010 - $args['count'] = $count;
2011 -
2012 - if ( ! $show_empty_terms && 0 === $count ) {
2013 - continue;
2014 - }
2015 -
2016 - $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 ) . '">';
2017 - $html .= '<div class="rtsb-default-filter-group' . esc_attr( $active_class ) . '">';
2018 - $html .= self::get_input_type_html( $input, $args );
2019 -
2020 - $counter++;
2021 -
2022 - $html .= '</div>';
2023 - $html .= '</li>';
2024 - }
2025 -
2026 - $html .= '</ul>';
2027 -
2028 - return $html;
2029 - }
2030 - /**
2031 - * Get input type HTML.
2032 - *
2033 - * @param string $input Input type.
2034 - * @param array $args Args.
2035 - * @return mixed|string|null
2036 - */
2037 - public static function get_input_type_html( $input, $args ) {
2038 - $html = '';
2039 - $count = $args['count'];
2040 -
2041 - switch ( $input ) {
2042 - case 'color':
2043 - $color = sanitize_hex_color( get_term_meta( $args['term_id'], 'product_attribute_color', true ) );
2044 -
2045 - $html .= sprintf(
2046 - // phpcs:ignore Generic.Strings.UnnecessaryStringConcat.Found
2047 - '<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>',
2048 - esc_attr( $args['id'] ),
2049 - esc_attr( $args['type'] ),
2050 - $args['tooltips'] ? esc_attr( ' tipsy' ) : '',
2051 - esc_attr( $color ),
2052 - esc_html( $args['name'] ),
2053 - absint( $count )
2054 - );
2055 - break;
2056 -
2057 - case 'image':
2058 - if ( ! function_exists( 'rtwpvs' ) ) {
2059 - return $html;
2060 - }
2061 -
2062 - $attachment_id = absint( get_term_meta( $args['term_id'], 'product_attribute_image', true ) );
2063 - $image_size = rtwpvs()->get_option( 'attribute_image_size' );
2064 - $image_url = wp_get_attachment_image_url( $attachment_id, apply_filters( 'rtsb_product_attribute_image_size', $image_size ) );
2065 -
2066 - $html .= sprintf(
2067 - // phpcs:ignore Generic.Strings.UnnecessaryStringConcat.Found
2068 - '<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>',
2069 - esc_attr( $args['id'] ),
2070 - esc_attr( $args['type'] ),
2071 - $args['tooltips'] ? esc_attr( ' tipsy' ) : '',
2072 - esc_attr( $args['name'] ),
2073 - esc_url( $image_url )
2074 - );
2075 - break;
2076 -
2077 - case 'button':
2078 - $html .= sprintf(
2079 - // phpcs:ignore Generic.Strings.UnnecessaryStringConcat.Found
2080 - '<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>',
2081 - esc_attr( $args['id'] ),
2082 - esc_attr( $args['type'] ),
2083 - $args['tooltips'] ? esc_attr( ' tipsy' ) : '',
2084 - esc_html( $args['name'] ),
2085 - absint( $count )
2086 - );
2087 - break;
2088 -
2089 - default:
2090 - }
2091 -
2092 - return apply_filters( 'rtsb/elementor/default/filter/get_input_type_html', $html );
2093 - }
2094 - /**
2095 - * Get reset button.
2096 - *
2097 - * @param string $btn_text Button text.
2098 - * @param array $settings Settings array.
2099 - *
2100 - * @return string
2101 - */
2102 - public static function get_default_filter_reset_button( $btn_text, $settings ) {
2103 - $html = '';
2104 -
2105 - ob_start();
2106 -
2107 - $is_editor = ! empty( \Elementor\Plugin::$instance->editor ) && \Elementor\Plugin::$instance->editor->is_edit_mode();
2108 - $active = ( $is_editor || ! empty( $_SERVER['QUERY_STRING'] ) ) ? ' active' : '';
2109 - $reset = ! empty( $settings['reset_btn'] );
2110 - $apply_text = ! empty( $settings['apply_filters_btn_text'] ) ? $settings['apply_filters_btn_text'] : esc_html__( 'Apply Filters', 'shopbuilder' );
2111 - $apply_icon = ! empty( $settings['apply_filters_btn_icon'] ) ? $settings['apply_filters_btn_icon'] : [];
2112 - echo '<div class="default-filter-btn-wrapper">';
2113 - ?>
2114 - <?php if ( $settings['show_filter_btn'] ) { ?>
2115 - <div class="rtsb-product-default-filters rtsb-apply-filters-btn">
2116 - <div class="reset-btn-item">
2117 - <button class="rtsb-apply-filters init">
2118 - <span class="icon"><?php Fns::print_html( Fns::icons_manager( $apply_icon, 'icon-default' ) ); ?></span>
2119 - <span class="text reset-text"><?php echo esc_html( $apply_text ); ?></span>
2120 - <span></span>
2121 - </button>
2122 - </div>
2123 - </div>
2124 - <?php } ?>
2125 - <?php
2126 - if ( $reset ) {
2127 - ?>
2128 - <div class="rtsb-product-default-filters rtsb-reset<?php echo esc_attr( $active ); ?>">
2129 - <div class="reset-btn-item">
2130 - <button class="product-default-filter-reset">
2131 - <span class="text reset-text"><?php echo esc_html( $btn_text ); ?></span>
2132 - <span></span>
2133 - </button>
2134 - </div>
2135 - </div>
2136 - <?php
2137 - }
2138 - echo '</div>';
2139 -
2140 - $html .= ob_get_clean();
2141 -
2142 - return $html;
2143 1008 }
2144 1009 }