PluginProbe
ShopBuilder – WooCommerce Builder For Elementor / 3.4.2
ShopBuilder – WooCommerce Builder For Elementor v3.4.2
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
shopbuilder / app / Elementor / Helper / RenderHelpers.php

RenderHelpers.php in ShopBuilder – WooCommerce Builder For Elementor 3.4.2, at app/Elementor/Helper/RenderHelpers.php

2,145 lines 74.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Elementor Render Helper Class.
4 *
5 * This class contains render helper logics.
6 *
7 * @package RadiusTheme\SB
8 */
9
10 namespace RadiusTheme\SB\Elementor\Helper;
11
12 use Elementor\Plugin;
13 use RadiusTheme\SB\Helpers\Cache;
14 use RadiusTheme\SB\Helpers\Fns;
15 use WC_Product_Query;
16 use WP_Term;
17
18 // Do not allow directly accessing this file.
19 if ( ! defined( 'ABSPATH' ) ) {
20 exit( 'This script cannot be accessed directly.' );
21 }
22
23 /**
24 * BuilderFns class
25 */
26 class RenderHelpers {
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 /**
53 * Get data.
54 *
55 * @param array|string $haystack The array or string to search for the value.
56 * @param string $needle The key to look for in the array or the string itself.
57 * @param mixed $default The default value to return if the key is not.
58 *
59 * @return mixed
60 */
61 public static function get_data( $haystack, $needle, $default = null ) {
62 if ( is_array( $haystack ) && ! empty( $haystack[ $needle ] ) ) {
63 return $haystack[ $needle ];
64 } elseif ( is_string( $haystack ) && ! empty( $haystack ) ) {
65 return $haystack;
66 } else {
67 return $default;
68 }
69 }
70
71 /**
72 * Builds an array with field values.
73 *
74 * @param array $meta Field values.
75 * @param string $template Template name.
76 * @param array $raw_settings Raw settings.
77 *
78 * @return array
79 */
80 public static function meta_dataset( array $meta, $template = '', $raw_settings = [] ) {
81 $c_image_size = self::get_data( $meta, 'image_custom_dimension', [] );
82 $c_image_size['crop'] = self::get_data( $meta, 'image_crop', [] );
83
84 if ( ( ! empty( $_GET['displayview'] ) && 'list' === $_GET['displayview'] ) || ( empty( $_GET['displayview'] ) && ( ! empty( $meta['view_mode'] ) && 'list' === $meta['view_mode'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
85 $meta['cols'] = self::get_data( $meta, 'list_cols', 1 );
86 $meta['cols_tablet'] = self::get_data( $meta, 'list_cols_tablet', $meta['cols'] );
87 $meta['cols_mobile'] = self::get_data( $meta, 'list_cols_mobile', $meta['cols'] );
88 }
89
90 $data = apply_filters(
91 'rtsb/elementor/render/meta_dataset',
92 [
93 // Layout.
94 'widget' => 'custom',
95 'template' => self::get_data( $template, '', '' ),
96 'raw_settings' => $raw_settings,
97 'layout' => self::get_data( $meta, 'layout', 'layout1' ),
98 'grid_layout' => self::get_data( $raw_settings, 'layout', 'grid-layout1' ),
99 'list_layout' => self::get_data( $raw_settings, 'list_layout', 'list-layout1' ),
100 'd_cols' => self::get_data( $meta, 'cols', 0 ),
101 't_cols' => self::get_data( $meta, 'cols_tablet', 2 ),
102 'm_cols' => self::get_data( $meta, 'cols_mobile', 1 ),
103 'grid_type' => self::get_data( $meta, 'grid_style', 'even' ),
104
105 // Query.
106 'post_in' => self::get_data( $meta, 'include_posts', [] ),
107 'post_not_in' => self::get_data( $meta, 'exclude_posts', [] ),
108 'limit' => ( empty( $meta['posts_limit'] ) || '-1' === $meta['posts_limit'] ) ? 10000000 : $meta['posts_limit'],
109 'offset' => self::get_data( $meta, 'posts_offset', 0 ),
110 'order_by' => self::get_data( $meta, 'posts_order_by', 'date' ),
111 'order' => self::get_data( $meta, 'posts_order', 'DESC' ),
112 'author_in' => self::get_data( $meta, 'filter_author', [] ),
113 'display_by' => self::get_data( $meta, 'products_filter', 'date' ),
114 'categories' => self::get_data( $meta, 'filter_categories', [] ),
115 'brands' => self::get_data( $meta, 'filter_brands', [] ),
116 'tags' => self::get_data( $meta, 'filter_tags', [] ),
117 'attributes' => self::get_data( $meta, 'filter_attributes', [] ),
118 'relation' => self::get_data( $meta, 'tax_relation', 'OR' ),
119 'category_source' => self::get_data( $meta, 'select_source', 'product_cat' ),
120 'category_term' => self::get_data( $meta, 'select_cat' ),
121 'display_cat_by' => self::get_data( $meta, 'display_cat_by' ),
122 'include_cats' => self::get_data( $meta, 'include_cats', [] ),
123 'exclude_cats' => self::get_data( $meta, 'exclude_cats', [] ),
124 'select_parent_cat' => self::get_data( $meta, 'select_parent_cat' ),
125 'select_cats' => self::get_data( $meta, 'select_cats', [] ),
126 'select_cat_ids' => self::get_data( $meta, 'include_cat_ids', '' ),
127 'cats_limit' => ( empty( $meta['cats_limit'] ) || '-1' === $meta['cats_limit'] ) ? 10000000 : $meta['cats_limit'],
128 'show_empty' => ! empty( $meta['show_empty'] ),
129 'show_uncategorized' => ! empty( $meta['show_uncategorized'] ),
130 'show_subcats' => ! empty( $meta['show_subcats'] ),
131 'show_top_level_cats' => ! empty( $meta['show_top_level_cats'] ),
132 'cats_order_by' => self::get_data( $meta, 'cats_order_by', 'name' ),
133 'cats_order' => self::get_data( $meta, 'cats_order', 'DESC' ),
134 'rtsb_order' => self::get_data( $meta, 'rtsb_order', 'ASC' ),
135 'rtsb_orderby' => self::get_data( $meta, 'rtsb_orderby', 'menu_order' ),
136
137 // Pagination.
138 'pagination' => ! empty( $meta['show_pagination'] ),
139 '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 ) ) ),
142
143 // Image.
144 'f_img' => ! empty( $meta['show_featured_image'] ),
145 'gallery_img' => ! empty( $meta['show_product_gallery'] ),
146 'c_img' => ! empty( $meta['show_cat_image'] ),
147 'hover_img' => ! empty( $meta['show_hover_image'] ),
148 'f_img_size' => self::get_data( $meta, 'image', 'medium' ),
149 'custom_img_size' => ! empty( $c_image_size ) && is_array( $c_image_size ) ? $c_image_size : [],
150 'default_img_id' => ! empty( $meta['default_preview_image']['id'] ) ? $meta['default_preview_image']['id'] : null,
151 'show_overlay' => ! empty( $meta['show_overlay'] ),
152 'hover_animation' => self::get_data( $meta, 'image_hover_animation', 'none' ),
153 'show_custom_image' => ! empty( $meta['show_custom_image'] ),
154 'custom_image' => ! empty( $meta['custom_image']['id'] ) ? $meta['custom_image']['id'] : null,
155
156 // Visibility.
157 'visibility' => ! empty( self::content_visibility( $meta ) ) ? self::content_visibility( $meta ) : [],
158
159 // Action Buttons.
160 'btn_preset' => self::get_data( $meta, 'action_btn_preset', 'preset1' ),
161 'btn_position' => self::get_data( $meta, 'action_btn_position', 'above' ),
162 'tooltip_position' => self::get_data( $meta, 'action_btn_tooltip_position', 'top' ),
163
164 // Product Title.
165 'title_tag' => self::get_data( $meta, 'title_tag', 'h3' ),
166 'title_hover' => ! empty( $meta['title_hover'] ),
167 'title_limit' => self::get_data( $meta, 'title_limit', 'default' ),
168 'title_limit_custom' => self::get_data( $meta, 'title_limit_custom' ),
169 'show_custom_title' => ! empty( $meta['show_custom_title'] ),
170 'cat_custom_title' => self::get_data( $meta, 'custom_title', '' ),
171
172 // Variation Swatches.
173 'swatch_position' => self::get_data( $meta, 'swatch_position', 'top' ),
174 'swatch_type' => self::get_data( $meta, 'swatch_type', 'circle' ),
175
176 // Short Description.
177 'show_custom_excerpt' => ! empty( $meta['show_custom_description'] ),
178 'cat_custom_excerpt' => self::get_data( $meta, 'custom_description', '' ),
179 'cat_excerpt_position' => self::get_data( $meta, 'excerpt_position', 'below' ),
180 'excerpt_limit' => self::get_data( $meta, 'excerpt_limit', 'default' ),
181 'excerpt_limit_custom' => self::get_data( $meta, 'excerpt_limit_custom', 200 ),
182
183 // Count.
184 'count_position' => self::get_data( $meta, 'count_display_type', 'flex' ),
185 'before_count' => self::get_data( $meta, 'before_count', '' ),
186 'after_count' => self::get_data( $meta, 'after_count', '' ),
187
188 // Add to cart.
189 'show_cart_icon' => ! empty( $meta['show_cart_icon'] ),
190 'show_cart_text' => ! empty( $meta['show_cart_text'] ),
191 'add_to_cart_icon' => self::get_data( $meta, 'add_to_cart_icon', [] ),
192 'add_to_cart_success_icon' => self::get_data( $meta, 'add_to_cart_success_icon', [] ),
193 'add_to_cart_text' => self::get_data( $meta, 'add_to_cart_text', esc_html__( 'Add to Cart', 'shopbuilder' ) ),
194 'add_to_cart_success_text' => self::get_data( $meta, 'add_to_cart_success_text', '' ),
195 'add_to_cart_alignment' => self::get_data( $meta, 'cart_icon_alignment', 'left' ),
196
197 // Action Button Icons.
198 'quick_view_icon' => self::get_data( $meta, 'quick_view_icon', [] ),
199 'comparison_icon' => self::get_data( $meta, 'comparison_icon', [] ),
200 'comparison_icon_added' => self::get_data( $meta, 'comparison_icon_added', [] ),
201 'wishlist_icon' => self::get_data( $meta, 'wishlist_icon', [] ),
202 'wishlist_icon_added' => self::get_data( $meta, 'wishlist_icon_added', [] ),
203
204 // Badges.
205 'sale_badge_type' => self::get_data( $meta, 'sale_badges_type', 'percentage' ),
206 'sale_badge_text' => self::get_data( $meta, 'sale_badges_text', esc_html__( 'Sale', 'shopbuilder' ) ),
207 'custom_badge_text' => self::get_data( $meta, 'custom_badge_text', esc_html__( 'Sale', 'shopbuilder' ) ),
208 'out_of_stock_badge_text' => self::get_data( $meta, 'stock_badges_text', esc_html__( 'Out of Stock', 'shopbuilder' ) ),
209 'badge_position' => self::get_data( $meta, 'badges_position', 'top' ),
210 'badge_alignment' => self::get_data( $meta, 'badges_alignment', 'left' ),
211 'badge_preset' => self::get_data( $meta, 'custom_badge_preset', 'preset-1' ),
212 'enable_badges_module' => self::get_data( $meta, 'enable_badges_module', '' ),
213
214 // Link.
215 'image_link' => ! empty( $meta['image_link'] ),
216 'title_link' => ! empty( $meta['title_link'] ),
217 'hover_btn_text' => self::get_data( $meta, 'hover_btn_text', esc_html__( 'See Details', 'shopbuilder' ) ),
218 'show_hover_btn_icon' => ! empty( $meta['show_hover_btn_icon'] ),
219 'hover_btn_icon' => self::get_data( $meta, 'hover_btn_icon', [] ),
220 'custom_link' => self::get_data( $meta, 'custom_link' ),
221
222 // Slider.
223 'd_group' => self::get_data( $meta, 'cols_group', 1 ),
224 't_group' => self::get_data( $meta, 'cols_group_tablet', 1 ),
225 'm_group' => self::get_data( $meta, 'cols_group_mobile', 1 ),
226 'auto_play' => ! empty( $meta['slide_autoplay'] ),
227 'stop_on_hover' => ! empty( $meta['pause_hover'] ),
228 'slider_nav' => ! empty( $meta['slider_nav'] ),
229 'slider_dot' => ! empty( $meta['slider_pagi'] ),
230 'slider_dynamic_dot' => ! empty( $meta['slider_dynamic_pagi'] ),
231 'loop' => ! empty( $meta['slider_loop'] ),
232 'lazy_load' => ! empty( $meta['slider_lazy_load'] ),
233 'auto_height' => ! empty( $meta['slider_auto_height'] ),
234 'speed' => self::get_data( $meta, 'slide_speed', 2000 ),
235 'space_between' => isset( $meta['grid_gap']['size'] ) && strlen( $meta['grid_gap']['size'] ) ? $meta['grid_gap']['size'] : 30,
236 'auto_play_timeout' => self::get_data( $meta, 'autoplay_timeout', 5000 ),
237 'nav_position' => self::get_data( $meta, 'slider_nav_position', 'top' ),
238 'left_arrow_icon' => self::get_data( $meta, 'slider_left_arrow_icon', [] ),
239 'right_arrow_icon' => self::get_data( $meta, 'slider_right_arrow_icon', [] ),
240 ],
241 $meta
242 );
243
244 if ( ! empty( $meta['active_cat_slider'] ) ) {
245 $data['active_cat_slider'] = true;
246 }
247
248 if ( ! empty( $meta['cols_widescreen'] ) ) {
249 $data['w_cols'] = $meta['cols_widescreen'];
250 }
251
252 if ( ! empty( $meta['cols_laptop'] ) ) {
253 $data['l_cols'] = $meta['cols_laptop'];
254 }
255
256 if ( ! empty( $meta['cols_tablet_extra'] ) ) {
257 $data['te_cols'] = $meta['cols_tablet_extra'];
258 }
259
260 if ( ! empty( $meta['cols_mobile_extra'] ) ) {
261 $data['me_cols'] = $meta['cols_mobile_extra'];
262 }
263
264 $queried_obj = get_queried_object();
265
266 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'] );
272 }
273
274 if ( ! is_shop() && is_product_taxonomy() ) {
275 if ( ! empty( $queried_obj->taxonomy ) ) {
276 $data['queried_tax'] = esc_html( $queried_obj->taxonomy );
277 }
278
279 if ( ! empty( $queried_obj->term_id ) ) {
280 $data['queried_term'] = esc_html( $queried_obj->term_id );
281 }
282 }
283
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 return apply_filters( 'rtsb/elementor/render/meta_dataset_final', $data, $meta, $raw_settings );
297 }
298
299 /**
300 * Builds an array with field values.
301 *
302 * @param array $meta Field values.
303 * @param string $template Template name.
304 *
305 * @return array
306 */
307 public static function archive_meta_dataset( array $meta, $template = '' ) {
308 $data = apply_filters(
309 'rtsb/elementor/render/archive_meta_dataset',
310 [
311 // Layout.
312 'widget' => 'default',
313 'template' => self::get_data( $template, '', '' ),
314 'view_mode' => self::get_data( $meta, 'view_mode', 'grid' ),
315 'show_flash_sale' => ! empty( $meta['show_flash_sale'] ),
316 'wishlist_button' => ! empty( $meta['wishlist_button'] ),
317 'comparison_button' => ! empty( $meta['comparison_button'] ),
318 'quick_view_button' => ! empty( $meta['quick_view_button'] ),
319 'show_rating' => ! empty( $meta['show_rating'] ),
320 '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 'cart_icon' => self::get_data( $meta, 'cart_icon', [] ),
326 'wishlist_icon' => self::get_data( $meta, 'wishlist_icon', [] ),
327 'wishlist_icon_added' => self::get_data( $meta, 'wishlist_icon_added', [] ),
328 'comparison_icon' => self::get_data( $meta, 'comparison_icon', [] ),
329 'comparison_icon_added' => self::get_data( $meta, 'comparison_icon_added', [] ),
330 'quick_view_icon' => self::get_data( $meta, 'quick_view_icon', [] ),
331 'prev_icon' => self::get_data( $meta, 'prev_icon', [] ),
332 'next_icon' => self::get_data( $meta, 'next_icon', [] ),
333 'posts_per_page' => self::get_products_per_page(),
334 'rtsb_order' => self::get_data( $meta, 'rtsb_order', 'ASC' ),
335 'rtsb_orderby' => self::get_data( $meta, 'rtsb_orderby', 'menu_order' ),
336 'tooltip_position' => self::get_data( $meta, 'action_btn_tooltip_position', 'top' ),
337 ],
338 $meta
339 );
340
341 $queried_obj = get_queried_object();
342
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() ) {
348 if ( ! empty( $queried_obj->taxonomy ) ) {
349 $data['queried_tax'] = esc_html( $queried_obj->taxonomy );
350 }
351
352 if ( ! empty( $queried_obj->term_id ) ) {
353 $data['queried_term'] = esc_html( $queried_obj->term_id );
354 }
355 }
356
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 return apply_filters( 'rtsb/elementor/render/archive_meta_dataset_final', $data, $meta );
370 }
371
372 /**
373 * Setting up content visibility
374 *
375 * @param array $settings Elementor settings.
376 *
377 * @return array
378 */
379 public static function content_visibility( $settings ) {
380 $visibility = [];
381
382 if ( ! empty( $settings['show_title'] ) ) {
383 $visibility[] = 'title';
384 }
385
386 if ( ! empty( $settings['show_short_desc'] ) ) {
387 $visibility[] = 'excerpt';
388 }
389
390 if ( ! empty( $settings['show_price'] ) ) {
391 $visibility[] = 'price';
392 }
393
394 if ( ! empty( $settings['show_rating'] ) ) {
395 $visibility[] = 'rating';
396 }
397
398 if ( ! empty( $settings['show_badges'] ) ) {
399 $visibility[] = 'badges';
400 }
401
402 if ( ! empty( $settings['show_categories'] ) ) {
403 $visibility[] = 'categories';
404 }
405
406 if ( ! empty( $settings['single_category'] ) ) {
407 $visibility[] = 'single-cat';
408 }
409 if ( ! empty( $settings['show_brands'] ) ) {
410 $visibility[] = 'brands';
411 }
412
413 if ( ! empty( $settings['single_brand'] ) ) {
414 $visibility[] = 'single-brand';
415 }
416
417 if ( ! empty( $settings['show_swatches'] ) ) {
418 $visibility[] = 'swatches';
419 }
420
421 if ( ! empty( $settings['show_vs_clear_btn'] ) ) {
422 $visibility[] = 'clear-btn';
423 }
424
425 if ( ! empty( $settings['show_count'] ) ) {
426 $visibility[] = 'count';
427 }
428
429 if ( ! empty( $settings['show_wishlist'] ) ) {
430 $visibility[] = 'wishlist';
431 }
432
433 if ( ! empty( $settings['show_compare'] ) ) {
434 $visibility[] = 'compare';
435 }
436
437 if ( ! empty( $settings['show_quick_view'] ) ) {
438 $visibility[] = 'quick_view';
439 }
440
441 if ( ! empty( $settings['show_add_to_cart'] ) ) {
442 $visibility[] = 'add_to_cart';
443 }
444
445 return $visibility;
446 }
447
448 /**
449 * Set Default Layout.
450 *
451 * @param string $layout Layout.
452 *
453 * @return string
454 */
455 public static function set_default_layout( $layout ) {
456 $is_list = preg_match( '/list/', $layout );
457 $is_cat_single = preg_match( '/category-single/', $layout );
458 $is_cat = preg_match( '/category/', $layout );
459 $is_carousel = preg_match( '/slider/', $layout );
460 $default = 'grid-layout1';
461
462 if ( $is_list ) {
463 $default = 'list-layout1';
464 } elseif ( $is_cat_single ) {
465 $default = 'category-single-layout1';
466 } elseif ( $is_cat ) {
467 $default = 'category-layout1';
468 } elseif ( $is_carousel ) {
469 $default = 'slider-layout1';
470 }
471
472 return $default;
473 }
474
475 /**
476 * Default layout columns.
477 *
478 * @param int $layout Layout.
479 *
480 * @return int
481 */
482 public static function default_columns( $layout ) {
483 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 case 'grid-layout2':
491 case 'slider-layout2':
492 case 'rtsb-coupon-layout1':
493 case 'rtsb-coupon-layout2':
494 case 'rtsb-coupon-layout3':
495 $columns = 3;
496 break;
497 case 'rtsb-post-list-layout1':
498 case 'rtsb-post-list-layout2':
499 case 'list-layout1':
500 case 'list-layout2':
501 case 'list-layout3':
502 case 'list-layout7':
503 case 'list-layout5':
504 $columns = 1;
505 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':
511 case 'list-layout4':
512 case 'list-layout6':
513 case 'slider-layout5':
514 case 'slider-layout8':
515 $columns = 2;
516 break;
517 case 'rtsb-logo-layout1':
518 case 'rtsb-logo-layout2':
519 case 'grid-layout6':
520 case 'grid-layout9':
521 $columns = 5;
522 break;
523
524 case 'category-layout1':
525 $columns = 6;
526 break;
527
528 default:
529 $columns = 4;
530 break;
531 }
532
533 return apply_filters( 'rtsb/element/general/default_columns', $columns, $layout );
534 }
535
536 /**
537 * Pagination JSON data.
538 *
539 * @param array $metas Data set.
540 * @param string $template Template name.
541 *
542 * @return string
543 */
544 public static function pagination_data( $metas, $template ) {
545 $el_data = self::meta_dataset( $metas, $template );
546
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 );
557 }
558
559 /**
560 * Container classes
561 *
562 * @param array $metas Meta data.
563 * @param array $settings Settings array.
564 * @param string $custom_class Custom class.
565 *
566 * @return mixed|null
567 */
568 public static function prepare_container_classes( $metas = [], $settings = [], $custom_class = '' ) {
569 $classes = 'rtsb-elementor-container products rtsb-pos-r ';
570 $classes .= $custom_class;
571
572 $raw_layout = esc_html( $metas['layout'] );
573 $layout = self::filter_layout( $raw_layout );
574 $is_cat = preg_match( '/category/', $layout );
575 $cart_txt_class = '';
576
577 $animation = $metas['hover_animation'];
578
579 if ( ! $is_cat ) {
580 $cart_txt_class = $metas['show_cart_text'] ? ' show-cart-text' : ' no-cart-text';
581 $animation .= ' gallery-hover-' . ( ! empty( $settings['gallery_hover_animation'] ) ? $settings['gallery_hover_animation'] : 'fade' );
582 }
583
584 $badge_class = [
585 'position' => $metas['badge_position'],
586 'alignment' => $metas['badge_alignment'],
587 ];
588
589 if ( ! in_array( 'clear-btn', $metas['visibility'], true ) ) {
590 $classes .= ' no-clear-btn';
591 }
592
593 $classes .= ! empty( $metas['pagination'] ) ? ' rtsb-has-pagination' : ' rtsb-no-pagination';
594 $classes .= ' badge-' . esc_attr( $badge_class['position'] ) . ' badge-' . esc_attr( $badge_class['alignment'] ) . esc_attr( $cart_txt_class ) . ' img-hover-' . esc_attr( $animation ) . ' excerpt-' . esc_attr( $metas['cat_excerpt_position'] );
595
596 if ( $metas['show_overlay'] ) {
597 $classes .= ' has-overlay';
598 }
599
600 if ( in_array( 'single-cat', $metas['visibility'], true ) ) {
601 $classes .= ' show-single-cat';
602 }
603 if ( in_array( 'single-brand', $metas['visibility'], true ) ) {
604 $classes .= ' show-single-brand';
605 }
606
607 if ( ! $is_cat ) {
608 $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',
616 'show_compare_icon' => 'no-compare-icon',
617 'show_quick_view_icon' => 'no-quick-view-icon',
618 'show_wishlist_icon' => 'no-wishlist-icon',
619 'show_quick_checkout_icon' => 'no-quick_checkout-icon',
620 ];
621
622 foreach ( $action_btn_classes as $setting_key => $class ) {
623 if ( strpos( $setting_key, 'icon' ) !== false ) {
624 $classes .= empty( $settings[ $setting_key ] ) ? ' ' . $class : '';
625 } else {
626 $classes .= ! empty( $settings[ $setting_key ] ) ? ' ' . $class : '';
627 }
628 }
629
630 if ( rtsb()->has_pro() && ! empty( $settings['custom_ordering'] ) ) {
631 $classes .= ' has-custom-ordering';
632 }
633 }
634 if ( rtsb()->has_pro() && ! empty( $settings['slider_slide_animation'] ) ) {
635 $classes .= ' has-slide-animation';
636 }
637
638 return apply_filters( 'rtsb/product/container/class', $classes, $settings );
639 }
640
641 /**
642 * Function to filter and validate the layout against allowed patterns.
643 *
644 * @param string $layout The layout string to be filtered.
645 * @param string $template The template to be checked.
646 *
647 * @return string
648 */
649 public static function filter_layout( $layout, $template = '' ) {
650 $allowed_patterns = apply_filters( 'rtsb/elementor/custom_layout', [ 'grid', 'list', 'slider', 'category', 'toyup', 'zilly', 'rtsb' ] );
651 $layout_part = explode( '-', $layout );
652 $default = 'grid-layout1';
653
654 if ( empty( $layout_part[0] ) ) {
655 return $default;
656 }
657
658 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 );
664 } else {
665 return ! empty( $template ) ? self::resolve_existing_layout( self::set_default_layout( $layout ), $template ) : $default;
666 }
667 }
668
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;
688 }
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 }
706
707 /**
708 * Row classes
709 *
710 * @param array $settings Settings array.
711 *
712 * @return mixed|null
713 */
714 public static function prepare_row_classes( $settings = [] ) {
715 $masonry_class = null;
716 $classes = 'rtsb-row rtsb-content-loader element-loading';
717
718 $loader_class = '';
719 if ( Fns::enable_loader() ) {
720 $loader_class = ' rtsb-pre-loader';
721 }
722 $raw_layout = esc_html( $settings['layout'] );
723 $layout = self::filter_layout( $raw_layout );
724 $grid_type = $settings['grid_type'];
725 $is_carousel = preg_match( '/slider/', $layout );
726
727 if ( preg_match( '/category/', $layout ) && ! empty( $settings['active_cat_slider'] ) ) {
728 $classes .= ' rtsb-slider-layout';
729 }
730
731 if ( 'equal' === $grid_type ) {
732 $masonry_class = ' rtsb-equal';
733 } elseif ( 'masonry' === $grid_type ) {
734 $masonry_class = ' rtsb-masonry';
735 } else {
736 $masonry_class = ' rtsb-even';
737 }
738
739 if ( ! rtsb()->has_pro() || $is_carousel ) {
740 $masonry_class = ' rtsb-even';
741 }
742
743 if ( $is_carousel && ! empty( $settings['raw_settings']['always_show_nav'] ) ) {
744 $classes .= ' always-show-nav';
745 }
746
747 if ( ! empty( $settings['raw_settings']['inner_slider_always_show_nav'] ) ) {
748 $classes .= ' inner-slider-always-show-nav';
749 }
750 if ( empty( $settings['raw_settings']['cols_mobile'] ) ) {
751 $classes .= ' rtsb-mobile-flex-row';
752 }
753
754 $classes .= ' rtsb-' . $layout . $masonry_class . $loader_class;
755
756 return apply_filters( 'rtsb/elementor/row/classes', $classes, $settings );
757 }
758
759 /**
760 * Prepare pagination attributes.
761 *
762 * @param string $layout The layout type.
763 * @param bool $has_filters Whether the widget has filters.
764 * @param string $template The template name.
765 * @param array $settings The widget settings.
766 * @param bool $ajax Whether to enable AJAX pagination.
767 *
768 * @return string
769 */
770 public static function prepare_pagination_attr( $layout, $has_filters, $template, $settings, $ajax ) {
771 $is_cat = preg_match( '/category/', $layout );
772 $is_carousel = preg_match( '/slider/', $layout );
773
774 if ( $is_carousel ) {
775 return $has_filters ? self::pagination_data( $settings, $template ) : '';
776 } elseif ( $is_cat ) {
777 return '';
778 } 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 );
783
784 return $condition ? self::pagination_data( $settings, $template ) : '';
785 }
786 }
787
788 /**
789 * Archive JSON data.
790 *
791 * @param array $metas Data set.
792 * @param string $template Template name.
793 *
794 * @return string
795 */
796 public static function archive_data( $metas, $template ) {
797 $el_data = self::archive_meta_dataset( $metas, $template );
798
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 &#039; 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 ) );
814 }
815
816 /**
817 * Slider options.
818 *
819 * @param array $meta Meta values.
820 * @param array $settings Raw Settings.
821 *
822 * @return array
823 */
824 public static function slider_data( array $meta, $settings = [] ) {
825 $has_dots = $meta['slider_dot'] ? ' has-dot' : ' no-dot';
826 $has_dots .= $meta['slider_nav'] ? ' has-nav' : ' no-nav';
827 $has_dynamic_dots = $meta['slider_dynamic_dot'] ? true : false;
828 $d_col = 0 === $meta['d_cols'] ? self::default_columns( $meta['layout'] ) : $meta['d_cols'];
829 $t_col = 0 === $meta['t_cols'] ? 2 : $meta['t_cols'];
830 $m_col = 0 === $meta['m_cols'] ? 1 : $meta['m_cols'];
831
832 if ( Plugin::$instance->breakpoints->has_custom_breakpoints() ) {
833 // Widescreen.
834 $w_col = self::get_data( $settings, 'cols_widescreen' );
835 $w_col = 0 === $w_col ? self::default_columns( $meta['layout'] ) : $w_col;
836 $w_group = self::get_data( $settings, 'cols_group_widescreen', 1 );
837
838 // Laptop.
839 $l_col = self::get_data( $settings, 'cols_laptop' );
840 $l_col = 0 === $l_col ? self::default_columns( $meta['layout'] ) : $l_col;
841 $l_group = self::get_data( $settings, 'cols_group_laptop', 1 );
842
843 // Tablet Landscape.
844 $te_col = self::get_data( $settings, 'cols_tablet_extra' );
845 $te_col = 0 === $te_col ? 3 : $te_col;
846 $te_group = self::get_data( $settings, 'cols_group_tablet_extra', 1 );
847
848 // Mobile Landscape.
849 $me_col = self::get_data( $settings, 'cols_mobile_extra' );
850 $me_col = 0 === $me_col ? 2 : $me_col;
851 $me_group = self::get_data( $settings, 'cols_group_mobile_extra', 1 );
852
853 $el_breakpoints = Fns::get_elementor_breakpoints();
854
855 foreach ( $el_breakpoints as $key => $value ) {
856 if ( isset( $value['is_enabled'] ) && ! $value['is_enabled'] ) {
857 unset( $el_breakpoints[ $key ] );
858 }
859 }
860
861 $breakpoints = [
862 0 => [
863 'slidesPerView' => absint( $m_col ),
864 'slidesPerGroup' => absint( $meta['m_group'] ),
865 'grid' => [
866 'rows' => 1,
867 ],
868 'pagination' => [
869 'dynamicBullets' => $has_dynamic_dots,
870 ],
871 ],
872 ];
873
874 $desktop = [
875 'desktop' => [
876 'label' => 'Desktop',
877 'value' => 1920,
878 'is_enabled' => 1,
879 ],
880 ];
881
882 if ( ! empty( $el_breakpoints['laptop'] ) && $el_breakpoints['laptop']['is_enabled'] ) {
883 $widescreen_position = array_search( 'widescreen', array_keys( $el_breakpoints ), true );
884 $el_breakpoints = array_merge(
885 array_slice( $el_breakpoints, 0, $widescreen_position ),
886 $desktop,
887 array_slice( $el_breakpoints, $widescreen_position )
888 );
889 } else {
890 $desktop['desktop']['value'] = 1366;
891 $el_breakpoints['desktop'] = $desktop['desktop'];
892 }
893
894 foreach ( $el_breakpoints as $el_breakpoint => $value ) {
895 $breakpoint_value = ! empty( $value['value'] ) ? $value['value'] : $value['default_value'];
896 $dynamic_bullets = $has_dynamic_dots;
897 $slides_per_view = $d_col;
898 $slides_per_group = $meta['d_group'];
899
900 switch ( $el_breakpoint ) {
901 case 'widescreen':
902 $slides_per_view = $w_col;
903 $slides_per_group = $w_group;
904
905 break;
906
907 case 'laptop':
908 $slides_per_view = $l_col;
909 $slides_per_group = $l_group;
910
911 break;
912
913 case 'tablet_extra':
914 $slides_per_view = $l_col;
915 $slides_per_group = $l_group;
916
917 if ( empty( $el_breakpoints['laptop'] ) ) {
918 $slides_per_view = $d_col;
919 $slides_per_group = $meta['d_group'];
920 }
921
922 break;
923
924 case 'tablet':
925 $slides_per_view = $te_col;
926 $slides_per_group = $te_group;
927
928 if ( empty( $el_breakpoints['laptop'] ) && empty( $el_breakpoints['tablet_extra'] ) ) {
929 $slides_per_view = $d_col;
930 $slides_per_group = $meta['d_group'];
931 } elseif ( empty( $el_breakpoints['laptop'] ) && ! empty( $el_breakpoints['tablet_extra'] ) ) {
932 $slides_per_view = $te_col;
933 $slides_per_group = $te_group;
934 } elseif ( ! empty( $el_breakpoints['laptop'] ) && empty( $el_breakpoints['tablet_extra'] ) ) {
935 $slides_per_view = $l_col;
936 $slides_per_group = $l_group;
937 }
938
939 break;
940
941 case 'mobile_extra':
942 $slides_per_view = $t_col;
943 $slides_per_group = $meta['t_group'];
944 $dynamic_bullets = $has_dynamic_dots;
945
946 break;
947
948 case 'mobile':
949 $slides_per_view = $t_col;
950 $slides_per_group = $meta['t_group'];
951 $dynamic_bullets = $has_dynamic_dots;
952
953 if ( ! empty( $el_breakpoints['mobile_extra'] ) ) {
954 $slides_per_view = $me_col;
955 $slides_per_group = $me_group;
956 }
957
958 break;
959 }
960
961 if ( $value['is_enabled'] ) {
962 $br = 'widescreen' !== $el_breakpoint ? $breakpoint_value + 1 : $breakpoint_value;
963
964 $breakpoints[ absint( $br ) ] = [
965 'slidesPerView' => absint( $slides_per_view ),
966 'slidesPerGroup' => absint( $slides_per_group ),
967 'pagination' => [
968 'dynamicBullets' => $dynamic_bullets,
969 ],
970 ];
971 }
972 }
973 } else {
974 $breakpoints = [
975 0 => [
976 'slidesPerView' => absint( $m_col ),
977 'slidesPerGroup' => absint( $meta['m_group'] ),
978 'pagination' => [
979 'dynamicBullets' => $has_dynamic_dots,
980 ],
981 ],
982 768 => [
983 'slidesPerView' => absint( $t_col ),
984 'slidesPerGroup' => absint( $meta['t_group'] ),
985 'pagination' => [
986 'dynamicBullets' => $has_dynamic_dots,
987 ],
988 ],
989 1025 => [
990 'slidesPerView' => absint( $d_col ),
991 'slidesPerGroup' => absint( $meta['d_group'] ),
992 'pagination' => [
993 'dynamicBullets' => $has_dynamic_dots,
994 ],
995 ],
996 ];
997 }
998
999 $slider_options = [
1000 'slidesPerView' => absint( $d_col ),
1001 'slidesPerGroup' => absint( $meta['d_group'] ),
1002 'speed' => absint( $meta['speed'] ),
1003 'loop' => $meta['loop'],
1004 'autoHeight' => $meta['auto_height'],
1005 'preloadImages' => ! $meta['lazy_load'],
1006 'lazy' => $meta['lazy_load'],
1007 'breakpoints' => $breakpoints,
1008 ];
1009
1010 if ( ! empty( $meta['raw_settings']['slider_slide_animation'] ) ) {
1011 $slider_options['watchSlidesProgress'] = true;
1012 }
1013 if ( $meta['auto_play'] ) {
1014 $slider_options['autoplay'] = [
1015 'delay' => absint( $meta['auto_play_timeout'] ),
1016 'pauseOnMouseEnter' => $meta['stop_on_hover'],
1017 'disableOnInteraction' => false,
1018 ];
1019 }
1020
1021 $slider_options = apply_filters( 'rtsb/elementor/render/slider_dataset', $slider_options, $meta );
1022 $carouselClass = 'rtsb-carousel-slider slider-loading rtsb-pos-s ' . $meta['nav_position'] . '-nav' . $has_dots;
1023
1024 return [
1025 'data' => esc_js( wp_json_encode( $slider_options ) ),
1026 'class' => $carouselClass,
1027 ];
1028 }
1029
1030 /**
1031 * Preloader.
1032 *
1033 * @param string $layout Layout.
1034 *
1035 * @return string
1036 */
1037 public static function pre_loader( $layout ) {
1038 $loader_class = '';
1039
1040 $is_carousel = preg_match( '/slider/', $layout );
1041
1042 if ( $is_carousel ) {
1043 $loader_class = ' full-op';
1044 }
1045 if ( ! Fns::enable_loader() ) {
1046 return '';
1047 }
1048 return '<div class="rtsb-elements-loading rtsb-ball-clip-rotate"><div></div></div>';
1049 }
1050
1051 /**
1052 * Badge class.
1053 *
1054 * @param string $preset Badge preset.
1055 *
1056 * @return string|null
1057 */
1058 public static function badge_class( $preset ) {
1059 switch ( $preset ) {
1060 case 'preset-default':
1061 $class = 'default-badge';
1062 break;
1063 case 'preset2':
1064 $class = 'fill angle-right';
1065 break;
1066 case 'preset3':
1067 $class = 'outline';
1068 break;
1069
1070 case 'preset4':
1071 $class = 'outline angle-right';
1072 break;
1073
1074 default:
1075 $class = 'fill';
1076 break;
1077 }
1078
1079 return $class;
1080 }
1081
1082 /**
1083 * Registers required scripts.
1084 *
1085 * @param array $scripts Scripts to register.
1086 *
1087 * @return void
1088 */
1089 public static function register_scripts( $scripts ) {
1090 $caro = false;
1091 $masonry = false;
1092 $script = [];
1093
1094 $script[] = 'jquery';
1095
1096 foreach ( $scripts as $sc => $value ) {
1097 if ( ! empty( $sc ) ) {
1098 if ( 'isCarousel' === $sc ) {
1099 $caro = $value;
1100 }
1101
1102 if ( 'isMasonry' === $sc ) {
1103 $masonry = $value;
1104 }
1105 }
1106 }
1107
1108 if ( count( $scripts ) ) {
1109 /**
1110 * Scripts.
1111 */
1112 if ( $caro ) {
1113 $script[] = 'swiper';
1114 }
1115
1116 if ( $masonry ) {
1117 $script[] = 'masonry';
1118 }
1119
1120 $script[] = 'imagesloaded';
1121 $script[] = Fns::optimized_handle( 'rtsb-public' );
1122
1123 foreach ( $script as $sc ) {
1124 wp_enqueue_script( $sc );
1125 }
1126 }
1127 }
1128
1129 /**
1130 * Check if a wrapper is needed based on the current theme.
1131 *
1132 * @return bool
1133 */
1134 public static function is_wrapper_needed() {
1135 $theme_list = apply_filters(
1136 'rtsb/products/inner/wrapper/basedon/themes',
1137 [
1138 'twentytwentyone',
1139 'twentytwentytwo',
1140 'twentytwentythree',
1141 'storefront',
1142 'hello-elementor',
1143 'astra',
1144 ],
1145 rtsb()->current_theme
1146 );
1147
1148 if ( in_array( rtsb()->current_theme, $theme_list, true ) ) {
1149 return true;
1150 }
1151
1152 return false;
1153 }
1154
1155 /**
1156 * Get the number of products per page based on WooCommerce settings.
1157 *
1158 * @return int
1159 */
1160 public static function get_products_per_page() {
1161 $products_row = absint( get_option( 'woocommerce_catalog_rows', 4 ) );
1162 $products_col = absint( get_option( 'woocommerce_catalog_columns', 4 ) );
1163 $products_per_page = apply_filters( 'rtsb/elementor/archive/products_per_page', $products_row * $products_col );
1164
1165 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 }
2144 }
2145