PluginProbe
ShopBuilder – WooCommerce Builder For Elementor / 3.4.1
ShopBuilder – WooCommerce Builder For Elementor v3.4.1
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.1, at app/Elementor/Helper/RenderHelpers.php

2,069 lines 70.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
142 // Image.
143 'f_img' => ! empty( $meta['show_featured_image'] ),
144 'gallery_img' => ! empty( $meta['show_product_gallery'] ),
145 'c_img' => ! empty( $meta['show_cat_image'] ),
146 'hover_img' => ! empty( $meta['show_hover_image'] ),
147 'f_img_size' => self::get_data( $meta, 'image', 'medium' ),
148 'custom_img_size' => ! empty( $c_image_size ) && is_array( $c_image_size ) ? $c_image_size : [],
149 'default_img_id' => ! empty( $meta['default_preview_image']['id'] ) ? $meta['default_preview_image']['id'] : null,
150 'show_overlay' => ! empty( $meta['show_overlay'] ),
151 'hover_animation' => self::get_data( $meta, 'image_hover_animation', 'none' ),
152 'show_custom_image' => ! empty( $meta['show_custom_image'] ),
153 'custom_image' => ! empty( $meta['custom_image']['id'] ) ? $meta['custom_image']['id'] : null,
154
155 // Visibility.
156 'visibility' => ! empty( self::content_visibility( $meta ) ) ? self::content_visibility( $meta ) : [],
157
158 // Action Buttons.
159 'btn_preset' => self::get_data( $meta, 'action_btn_preset', 'preset1' ),
160 'btn_position' => self::get_data( $meta, 'action_btn_position', 'above' ),
161 'tooltip_position' => self::get_data( $meta, 'action_btn_tooltip_position', 'top' ),
162
163 // Product Title.
164 'title_tag' => self::get_data( $meta, 'title_tag', 'h3' ),
165 'title_hover' => ! empty( $meta['title_hover'] ),
166 'title_limit' => self::get_data( $meta, 'title_limit', 'default' ),
167 'title_limit_custom' => self::get_data( $meta, 'title_limit_custom' ),
168 'show_custom_title' => ! empty( $meta['show_custom_title'] ),
169 'cat_custom_title' => self::get_data( $meta, 'custom_title', '' ),
170
171 // Variation Swatches.
172 'swatch_position' => self::get_data( $meta, 'swatch_position', 'top' ),
173 'swatch_type' => self::get_data( $meta, 'swatch_type', 'circle' ),
174
175 // Short Description.
176 'show_custom_excerpt' => ! empty( $meta['show_custom_description'] ),
177 'cat_custom_excerpt' => self::get_data( $meta, 'custom_description', '' ),
178 'cat_excerpt_position' => self::get_data( $meta, 'excerpt_position', 'below' ),
179 'excerpt_limit' => self::get_data( $meta, 'excerpt_limit', 'default' ),
180 'excerpt_limit_custom' => self::get_data( $meta, 'excerpt_limit_custom', 200 ),
181
182 // Count.
183 'count_position' => self::get_data( $meta, 'count_display_type', 'flex' ),
184 'before_count' => self::get_data( $meta, 'before_count', '' ),
185 'after_count' => self::get_data( $meta, 'after_count', '' ),
186
187 // Add to cart.
188 'show_cart_icon' => ! empty( $meta['show_cart_icon'] ),
189 'show_cart_text' => ! empty( $meta['show_cart_text'] ),
190 'add_to_cart_icon' => self::get_data( $meta, 'add_to_cart_icon', [] ),
191 'add_to_cart_success_icon' => self::get_data( $meta, 'add_to_cart_success_icon', [] ),
192 'add_to_cart_text' => self::get_data( $meta, 'add_to_cart_text', esc_html__( 'Add to Cart', 'shopbuilder' ) ),
193 'add_to_cart_success_text' => self::get_data( $meta, 'add_to_cart_success_text', '' ),
194 'add_to_cart_alignment' => self::get_data( $meta, 'cart_icon_alignment', 'left' ),
195
196 // Action Button Icons.
197 'quick_view_icon' => self::get_data( $meta, 'quick_view_icon', [] ),
198 'comparison_icon' => self::get_data( $meta, 'comparison_icon', [] ),
199 'comparison_icon_added' => self::get_data( $meta, 'comparison_icon_added', [] ),
200 'wishlist_icon' => self::get_data( $meta, 'wishlist_icon', [] ),
201 'wishlist_icon_added' => self::get_data( $meta, 'wishlist_icon_added', [] ),
202
203 // Badges.
204 'sale_badge_type' => self::get_data( $meta, 'sale_badges_type', 'percentage' ),
205 'sale_badge_text' => self::get_data( $meta, 'sale_badges_text', esc_html__( 'Sale', 'shopbuilder' ) ),
206 'custom_badge_text' => self::get_data( $meta, 'custom_badge_text', esc_html__( 'Sale', 'shopbuilder' ) ),
207 'out_of_stock_badge_text' => self::get_data( $meta, 'stock_badges_text', esc_html__( 'Out of Stock', 'shopbuilder' ) ),
208 'badge_position' => self::get_data( $meta, 'badges_position', 'top' ),
209 'badge_alignment' => self::get_data( $meta, 'badges_alignment', 'left' ),
210 'badge_preset' => self::get_data( $meta, 'custom_badge_preset', 'preset-1' ),
211 'enable_badges_module' => self::get_data( $meta, 'enable_badges_module', '' ),
212
213 // Link.
214 'image_link' => ! empty( $meta['image_link'] ),
215 'title_link' => ! empty( $meta['title_link'] ),
216 'hover_btn_text' => self::get_data( $meta, 'hover_btn_text', esc_html__( 'See Details', 'shopbuilder' ) ),
217 'show_hover_btn_icon' => ! empty( $meta['show_hover_btn_icon'] ),
218 'hover_btn_icon' => self::get_data( $meta, 'hover_btn_icon', [] ),
219 'custom_link' => self::get_data( $meta, 'custom_link' ),
220
221 // Slider.
222 'd_group' => self::get_data( $meta, 'cols_group', 1 ),
223 't_group' => self::get_data( $meta, 'cols_group_tablet', 1 ),
224 'm_group' => self::get_data( $meta, 'cols_group_mobile', 1 ),
225 'auto_play' => ! empty( $meta['slide_autoplay'] ),
226 'stop_on_hover' => ! empty( $meta['pause_hover'] ),
227 'slider_nav' => ! empty( $meta['slider_nav'] ),
228 'slider_dot' => ! empty( $meta['slider_pagi'] ),
229 'slider_dynamic_dot' => ! empty( $meta['slider_dynamic_pagi'] ),
230 'loop' => ! empty( $meta['slider_loop'] ),
231 'lazy_load' => ! empty( $meta['slider_lazy_load'] ),
232 'auto_height' => ! empty( $meta['slider_auto_height'] ),
233 'speed' => self::get_data( $meta, 'slide_speed', 2000 ),
234 'space_between' => isset( $meta['grid_gap']['size'] ) && strlen( $meta['grid_gap']['size'] ) ? $meta['grid_gap']['size'] : 30,
235 'auto_play_timeout' => self::get_data( $meta, 'autoplay_timeout', 5000 ),
236 'nav_position' => self::get_data( $meta, 'slider_nav_position', 'top' ),
237 'left_arrow_icon' => self::get_data( $meta, 'slider_left_arrow_icon', [] ),
238 'right_arrow_icon' => self::get_data( $meta, 'slider_right_arrow_icon', [] ),
239 ],
240 $meta
241 );
242
243 if ( ! empty( $meta['active_cat_slider'] ) ) {
244 $data['active_cat_slider'] = true;
245 }
246
247 if ( ! empty( $meta['cols_widescreen'] ) ) {
248 $data['w_cols'] = $meta['cols_widescreen'];
249 }
250
251 if ( ! empty( $meta['cols_laptop'] ) ) {
252 $data['l_cols'] = $meta['cols_laptop'];
253 }
254
255 if ( ! empty( $meta['cols_tablet_extra'] ) ) {
256 $data['te_cols'] = $meta['cols_tablet_extra'];
257 }
258
259 if ( ! empty( $meta['cols_mobile_extra'] ) ) {
260 $data['me_cols'] = $meta['cols_mobile_extra'];
261 }
262
263 $queried_obj = get_queried_object();
264
265 if ( is_shop() || is_product_taxonomy() ) {
266 if ( empty( $data['posts_per_page'] ) ) {
267 $data['posts_per_page'] = self::get_products_per_page();
268 }
269 $data['order_by'] = self::get_data( $data, 'rtsb_orderby', $data['order_by'] );
270 $data['order'] = self::get_data( $data, 'rtsb_order', $data['order'] );
271 }
272
273 if ( ! is_shop() && is_product_taxonomy() ) {
274 if ( ! empty( $queried_obj->taxonomy ) ) {
275 $data['queried_tax'] = esc_html( $queried_obj->taxonomy );
276 }
277
278 if ( ! empty( $queried_obj->term_id ) ) {
279 $data['queried_term'] = esc_html( $queried_obj->term_id );
280 }
281 }
282
283 if ( is_search() ) {
284 $search_query = get_search_query();
285
286 if ( $search_query ) {
287 $data['search_query'] = sanitize_text_field( $search_query );
288 }
289 }
290
291 return apply_filters( 'rtsb/elementor/render/meta_dataset_final', $data, $meta, $raw_settings );
292 }
293
294 /**
295 * Builds an array with field values.
296 *
297 * @param array $meta Field values.
298 * @param string $template Template name.
299 *
300 * @return array
301 */
302 public static function archive_meta_dataset( array $meta, $template = '' ) {
303 $data = apply_filters(
304 'rtsb/elementor/render/archive_meta_dataset',
305 [
306 // Layout.
307 'widget' => 'default',
308 'template' => self::get_data( $template, '', '' ),
309 'view_mode' => self::get_data( $meta, 'view_mode', 'grid' ),
310 'show_flash_sale' => ! empty( $meta['show_flash_sale'] ),
311 'wishlist_button' => ! empty( $meta['wishlist_button'] ),
312 'comparison_button' => ! empty( $meta['comparison_button'] ),
313 'quick_view_button' => ! empty( $meta['quick_view_button'] ),
314 'show_rating' => ! empty( $meta['show_rating'] ),
315 'show_pagination' => ! empty( $meta['show_pagination'] ),
316 // The base Product Archive widget has no pagination-type control: with filters it
317 // uses Ajax Load More (see the widget's pagination notice). Custom-layout widgets
318 // pass their own pagination_type and use a different dataset.
319 'posts_loading_type' => self::get_data( $meta, 'pagination_type', 'load_more' ),
320 'cart_icon' => self::get_data( $meta, 'cart_icon', [] ),
321 'wishlist_icon' => self::get_data( $meta, 'wishlist_icon', [] ),
322 'wishlist_icon_added' => self::get_data( $meta, 'wishlist_icon_added', [] ),
323 'comparison_icon' => self::get_data( $meta, 'comparison_icon', [] ),
324 'comparison_icon_added' => self::get_data( $meta, 'comparison_icon_added', [] ),
325 'quick_view_icon' => self::get_data( $meta, 'quick_view_icon', [] ),
326 'prev_icon' => self::get_data( $meta, 'prev_icon', [] ),
327 'next_icon' => self::get_data( $meta, 'next_icon', [] ),
328 'posts_per_page' => self::get_products_per_page(),
329 'rtsb_order' => self::get_data( $meta, 'rtsb_order', 'ASC' ),
330 'rtsb_orderby' => self::get_data( $meta, 'rtsb_orderby', 'menu_order' ),
331 'tooltip_position' => self::get_data( $meta, 'action_btn_tooltip_position', 'top' ),
332 ],
333 $meta
334 );
335
336 $queried_obj = get_queried_object();
337
338 // Use WooCommerce's own taxonomy check (covers product_cat, product_tag
339 // and product_brand) so the queried term is recorded on every product
340 // taxonomy archive — Ajax load-more / filters scope to it. Mirrors the
341 // custom-layout dataset in self::meta_dataset().
342 if ( ! is_shop() && is_product_taxonomy() ) {
343 if ( ! empty( $queried_obj->taxonomy ) ) {
344 $data['queried_tax'] = esc_html( $queried_obj->taxonomy );
345 }
346
347 if ( ! empty( $queried_obj->term_id ) ) {
348 $data['queried_term'] = esc_html( $queried_obj->term_id );
349 }
350 }
351
352 if ( is_search() ) {
353 $search_query = get_search_query();
354
355 if ( $search_query ) {
356 $data['search_query'] = sanitize_text_field( $search_query );
357 }
358 }
359
360 return apply_filters( 'rtsb/elementor/render/archive_meta_dataset_final', $data, $meta );
361 }
362
363 /**
364 * Setting up content visibility
365 *
366 * @param array $settings Elementor settings.
367 *
368 * @return array
369 */
370 public static function content_visibility( $settings ) {
371 $visibility = [];
372
373 if ( ! empty( $settings['show_title'] ) ) {
374 $visibility[] = 'title';
375 }
376
377 if ( ! empty( $settings['show_short_desc'] ) ) {
378 $visibility[] = 'excerpt';
379 }
380
381 if ( ! empty( $settings['show_price'] ) ) {
382 $visibility[] = 'price';
383 }
384
385 if ( ! empty( $settings['show_rating'] ) ) {
386 $visibility[] = 'rating';
387 }
388
389 if ( ! empty( $settings['show_badges'] ) ) {
390 $visibility[] = 'badges';
391 }
392
393 if ( ! empty( $settings['show_categories'] ) ) {
394 $visibility[] = 'categories';
395 }
396
397 if ( ! empty( $settings['single_category'] ) ) {
398 $visibility[] = 'single-cat';
399 }
400 if ( ! empty( $settings['show_brands'] ) ) {
401 $visibility[] = 'brands';
402 }
403
404 if ( ! empty( $settings['single_brand'] ) ) {
405 $visibility[] = 'single-brand';
406 }
407
408 if ( ! empty( $settings['show_swatches'] ) ) {
409 $visibility[] = 'swatches';
410 }
411
412 if ( ! empty( $settings['show_vs_clear_btn'] ) ) {
413 $visibility[] = 'clear-btn';
414 }
415
416 if ( ! empty( $settings['show_count'] ) ) {
417 $visibility[] = 'count';
418 }
419
420 if ( ! empty( $settings['show_wishlist'] ) ) {
421 $visibility[] = 'wishlist';
422 }
423
424 if ( ! empty( $settings['show_compare'] ) ) {
425 $visibility[] = 'compare';
426 }
427
428 if ( ! empty( $settings['show_quick_view'] ) ) {
429 $visibility[] = 'quick_view';
430 }
431
432 if ( ! empty( $settings['show_add_to_cart'] ) ) {
433 $visibility[] = 'add_to_cart';
434 }
435
436 return $visibility;
437 }
438
439 /**
440 * Set Default Layout.
441 *
442 * @param string $layout Layout.
443 *
444 * @return string
445 */
446 public static function set_default_layout( $layout ) {
447 $is_list = preg_match( '/list/', $layout );
448 $is_cat_single = preg_match( '/category-single/', $layout );
449 $is_cat = preg_match( '/category/', $layout );
450 $is_carousel = preg_match( '/slider/', $layout );
451 $default = 'grid-layout1';
452
453 if ( $is_list ) {
454 $default = 'list-layout1';
455 } elseif ( $is_cat_single ) {
456 $default = 'category-single-layout1';
457 } elseif ( $is_cat ) {
458 $default = 'category-layout1';
459 } elseif ( $is_carousel ) {
460 $default = 'slider-layout1';
461 }
462
463 return $default;
464 }
465
466 /**
467 * Default layout columns.
468 *
469 * @param int $layout Layout.
470 *
471 * @return int
472 */
473 public static function default_columns( $layout ) {
474 switch ( $layout ) {
475 case 'rtsb-post-grid-layout1':
476 case 'rtsb-post-grid-layout2':
477 case 'rtsb-post-grid-layout3':
478 case 'rtsb-team-layout1':
479 case 'rtsb-team-layout2':
480 case 'rtsb-testimonial-layout2':
481 case 'grid-layout2':
482 case 'slider-layout2':
483 case 'rtsb-coupon-layout1':
484 case 'rtsb-coupon-layout2':
485 case 'rtsb-coupon-layout3':
486 $columns = 3;
487 break;
488 case 'rtsb-post-list-layout1':
489 case 'rtsb-post-list-layout2':
490 case 'list-layout1':
491 case 'list-layout2':
492 case 'list-layout3':
493 case 'list-layout7':
494 case 'list-layout5':
495 $columns = 1;
496 break;
497 case 'rtsb-team-layout4':
498 case 'rtsb-testimonial-layout1':
499 case 'rtsb-testimonial-layout3':
500 case 'rtsb-testimonial-layout4':
501 case 'rtsb-testimonial-layout5':
502 case 'list-layout4':
503 case 'list-layout6':
504 case 'slider-layout5':
505 case 'slider-layout8':
506 $columns = 2;
507 break;
508 case 'rtsb-logo-layout1':
509 case 'rtsb-logo-layout2':
510 case 'grid-layout6':
511 case 'grid-layout9':
512 $columns = 5;
513 break;
514
515 case 'category-layout1':
516 $columns = 6;
517 break;
518
519 default:
520 $columns = 4;
521 break;
522 }
523
524 return apply_filters( 'rtsb/element/general/default_columns', $columns, $layout );
525 }
526
527 /**
528 * Pagination JSON data.
529 *
530 * @param array $metas Data set.
531 * @param string $template Template name.
532 *
533 * @return string
534 */
535 public static function pagination_data( $metas, $template ) {
536 $el_data = self::meta_dataset( $metas, $template );
537
538 return esc_js( wp_json_encode( $el_data, JSON_UNESCAPED_UNICODE ) );
539 }
540
541 /**
542 * Container classes
543 *
544 * @param array $metas Meta data.
545 * @param array $settings Settings array.
546 * @param string $custom_class Custom class.
547 *
548 * @return mixed|null
549 */
550 public static function prepare_container_classes( $metas = [], $settings = [], $custom_class = '' ) {
551 $classes = 'rtsb-elementor-container products rtsb-pos-r ';
552 $classes .= $custom_class;
553
554 $raw_layout = esc_html( $metas['layout'] );
555 $layout = self::filter_layout( $raw_layout );
556 $is_cat = preg_match( '/category/', $layout );
557 $cart_txt_class = '';
558
559 $animation = $metas['hover_animation'];
560
561 if ( ! $is_cat ) {
562 $cart_txt_class = $metas['show_cart_text'] ? ' show-cart-text' : ' no-cart-text';
563 $animation .= ' gallery-hover-' . ( ! empty( $settings['gallery_hover_animation'] ) ? $settings['gallery_hover_animation'] : 'fade' );
564 }
565
566 $badge_class = [
567 'position' => $metas['badge_position'],
568 'alignment' => $metas['badge_alignment'],
569 ];
570
571 if ( ! in_array( 'clear-btn', $metas['visibility'], true ) ) {
572 $classes .= ' no-clear-btn';
573 }
574
575 $classes .= ! empty( $metas['pagination'] ) ? ' rtsb-has-pagination' : ' rtsb-no-pagination';
576 $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'] );
577
578 if ( $metas['show_overlay'] ) {
579 $classes .= ' has-overlay';
580 }
581
582 if ( in_array( 'single-cat', $metas['visibility'], true ) ) {
583 $classes .= ' show-single-cat';
584 }
585 if ( in_array( 'single-brand', $metas['visibility'], true ) ) {
586 $classes .= ' show-single-brand';
587 }
588
589 if ( ! $is_cat ) {
590 $action_btn_classes = [
591 'show_compare_text' => 'show-compare-text',
592 'show_quick_view_text' => 'show-quick-view-text',
593 'show_wishlist_text' => 'show-wishlist-text',
594 'show_compare_icon' => 'no-compare-icon',
595 'show_quick_view_icon' => 'no-quick-view-icon',
596 'show_wishlist_icon' => 'no-wishlist-icon',
597 'show_quick_checkout_icon' => 'no-quick_checkout-icon',
598 ];
599
600 foreach ( $action_btn_classes as $setting_key => $class ) {
601 if ( strpos( $setting_key, 'icon' ) !== false ) {
602 $classes .= empty( $settings[ $setting_key ] ) ? ' ' . $class : '';
603 } else {
604 $classes .= ! empty( $settings[ $setting_key ] ) ? ' ' . $class : '';
605 }
606 }
607
608 if ( rtsb()->has_pro() && ! empty( $settings['custom_ordering'] ) ) {
609 $classes .= ' has-custom-ordering';
610 }
611 }
612 if ( rtsb()->has_pro() && ! empty( $settings['slider_slide_animation'] ) ) {
613 $classes .= ' has-slide-animation';
614 }
615
616 return apply_filters( 'rtsb/product/container/class', $classes, $settings );
617 }
618
619 /**
620 * Function to filter and validate the layout against allowed patterns.
621 *
622 * @param string $layout The layout string to be filtered.
623 * @param string $template The template to be checked.
624 *
625 * @return string
626 */
627 public static function filter_layout( $layout, $template = '' ) {
628 $allowed_patterns = apply_filters( 'rtsb/elementor/custom_layout', [ 'grid', 'list', 'slider', 'category', 'toyup', 'zilly', 'rtsb' ] );
629 $layout_part = explode( '-', $layout );
630 $default = 'grid-layout1';
631
632 if ( empty( $layout_part[0] ) ) {
633 return $default;
634 }
635
636 if ( in_array( $layout_part[0], $allowed_patterns, true ) ) {
637 if ( ! rtsb()->has_pro() && ! in_array( $layout, array_keys( Fns::free_layouts( $layout ) ), true ) ) {
638 $layout = self::set_default_layout( $layout );
639 }
640
641 return $layout;
642 } else {
643 return ! empty( $template ) ? self::set_default_layout( $layout ) : $default;
644 }
645 }
646
647 /**
648 * Row classes
649 *
650 * @param array $settings Settings array.
651 *
652 * @return mixed|null
653 */
654 public static function prepare_row_classes( $settings = [] ) {
655 $masonry_class = null;
656 $classes = 'rtsb-row rtsb-content-loader element-loading';
657
658 $loader_class = '';
659 if ( Fns::enable_loader() ) {
660 $loader_class = ' rtsb-pre-loader';
661 }
662 $raw_layout = esc_html( $settings['layout'] );
663 $layout = self::filter_layout( $raw_layout );
664 $grid_type = $settings['grid_type'];
665 $is_carousel = preg_match( '/slider/', $layout );
666
667 if ( preg_match( '/category/', $layout ) && ! empty( $settings['active_cat_slider'] ) ) {
668 $classes .= ' rtsb-slider-layout';
669 }
670
671 if ( 'equal' === $grid_type ) {
672 $masonry_class = ' rtsb-equal';
673 } elseif ( 'masonry' === $grid_type ) {
674 $masonry_class = ' rtsb-masonry';
675 } else {
676 $masonry_class = ' rtsb-even';
677 }
678
679 if ( ! rtsb()->has_pro() || $is_carousel ) {
680 $masonry_class = ' rtsb-even';
681 }
682
683 if ( $is_carousel && ! empty( $settings['raw_settings']['always_show_nav'] ) ) {
684 $classes .= ' always-show-nav';
685 }
686
687 if ( ! empty( $settings['raw_settings']['inner_slider_always_show_nav'] ) ) {
688 $classes .= ' inner-slider-always-show-nav';
689 }
690 if ( empty( $settings['raw_settings']['cols_mobile'] ) ) {
691 $classes .= ' rtsb-mobile-flex-row';
692 }
693
694 $classes .= ' rtsb-' . $layout . $masonry_class . $loader_class;
695
696 return apply_filters( 'rtsb/elementor/row/classes', $classes, $settings );
697 }
698
699 /**
700 * Prepare pagination attributes.
701 *
702 * @param string $layout The layout type.
703 * @param bool $has_filters Whether the widget has filters.
704 * @param string $template The template name.
705 * @param array $settings The widget settings.
706 * @param bool $ajax Whether to enable AJAX pagination.
707 *
708 * @return string
709 */
710 public static function prepare_pagination_attr( $layout, $has_filters, $template, $settings, $ajax ) {
711 $is_cat = preg_match( '/category/', $layout );
712 $is_carousel = preg_match( '/slider/', $layout );
713
714 if ( $is_carousel ) {
715 return $has_filters ? self::pagination_data( $settings, $template ) : '';
716 } elseif ( $is_cat ) {
717 return '';
718 } else {
719 $ajax_pagination = ! empty( $settings['show_pagination'] ) && 'pagination' !== $settings['pagination_type'];
720 $page = Fns::product_filters_has_ajax( apply_filters( 'rtsb/builder/set/current/page/type', '' ) );
721 $is_shop_or_archive = is_shop() || is_product_taxonomy();
722 $condition = rtsb()->has_pro() && $ajax && ( $ajax_pagination || $has_filters || $page || $is_shop_or_archive );
723
724 return $condition ? self::pagination_data( $settings, $template ) : '';
725 }
726 }
727
728 /**
729 * Archive JSON data.
730 *
731 * @param array $metas Data set.
732 * @param string $template Template name.
733 *
734 * @return string
735 */
736 public static function archive_data( $metas, $template ) {
737 $el_data = self::archive_meta_dataset( $metas, $template );
738
739 return ' data-rtsb-ajax=\'' . esc_js( wp_json_encode( $el_data ) ) . '\'';
740 }
741
742 /**
743 * Slider options.
744 *
745 * @param array $meta Meta values.
746 * @param array $settings Raw Settings.
747 *
748 * @return array
749 */
750 public static function slider_data( array $meta, $settings = [] ) {
751 $has_dots = $meta['slider_dot'] ? ' has-dot' : ' no-dot';
752 $has_dots .= $meta['slider_nav'] ? ' has-nav' : ' no-nav';
753 $has_dynamic_dots = $meta['slider_dynamic_dot'] ? true : false;
754 $d_col = 0 === $meta['d_cols'] ? self::default_columns( $meta['layout'] ) : $meta['d_cols'];
755 $t_col = 0 === $meta['t_cols'] ? 2 : $meta['t_cols'];
756 $m_col = 0 === $meta['m_cols'] ? 1 : $meta['m_cols'];
757
758 if ( Plugin::$instance->breakpoints->has_custom_breakpoints() ) {
759 // Widescreen.
760 $w_col = self::get_data( $settings, 'cols_widescreen' );
761 $w_col = 0 === $w_col ? self::default_columns( $meta['layout'] ) : $w_col;
762 $w_group = self::get_data( $settings, 'cols_group_widescreen', 1 );
763
764 // Laptop.
765 $l_col = self::get_data( $settings, 'cols_laptop' );
766 $l_col = 0 === $l_col ? self::default_columns( $meta['layout'] ) : $l_col;
767 $l_group = self::get_data( $settings, 'cols_group_laptop', 1 );
768
769 // Tablet Landscape.
770 $te_col = self::get_data( $settings, 'cols_tablet_extra' );
771 $te_col = 0 === $te_col ? 3 : $te_col;
772 $te_group = self::get_data( $settings, 'cols_group_tablet_extra', 1 );
773
774 // Mobile Landscape.
775 $me_col = self::get_data( $settings, 'cols_mobile_extra' );
776 $me_col = 0 === $me_col ? 2 : $me_col;
777 $me_group = self::get_data( $settings, 'cols_group_mobile_extra', 1 );
778
779 $el_breakpoints = Fns::get_elementor_breakpoints();
780
781 foreach ( $el_breakpoints as $key => $value ) {
782 if ( isset( $value['is_enabled'] ) && ! $value['is_enabled'] ) {
783 unset( $el_breakpoints[ $key ] );
784 }
785 }
786
787 $breakpoints = [
788 0 => [
789 'slidesPerView' => absint( $m_col ),
790 'slidesPerGroup' => absint( $meta['m_group'] ),
791 'grid' => [
792 'rows' => 1,
793 ],
794 'pagination' => [
795 'dynamicBullets' => $has_dynamic_dots,
796 ],
797 ],
798 ];
799
800 $desktop = [
801 'desktop' => [
802 'label' => 'Desktop',
803 'value' => 1920,
804 'is_enabled' => 1,
805 ],
806 ];
807
808 if ( ! empty( $el_breakpoints['laptop'] ) && $el_breakpoints['laptop']['is_enabled'] ) {
809 $widescreen_position = array_search( 'widescreen', array_keys( $el_breakpoints ), true );
810 $el_breakpoints = array_merge(
811 array_slice( $el_breakpoints, 0, $widescreen_position ),
812 $desktop,
813 array_slice( $el_breakpoints, $widescreen_position )
814 );
815 } else {
816 $desktop['desktop']['value'] = 1366;
817 $el_breakpoints['desktop'] = $desktop['desktop'];
818 }
819
820 foreach ( $el_breakpoints as $el_breakpoint => $value ) {
821 $breakpoint_value = ! empty( $value['value'] ) ? $value['value'] : $value['default_value'];
822 $dynamic_bullets = $has_dynamic_dots;
823 $slides_per_view = $d_col;
824 $slides_per_group = $meta['d_group'];
825
826 switch ( $el_breakpoint ) {
827 case 'widescreen':
828 $slides_per_view = $w_col;
829 $slides_per_group = $w_group;
830
831 break;
832
833 case 'laptop':
834 $slides_per_view = $l_col;
835 $slides_per_group = $l_group;
836
837 break;
838
839 case 'tablet_extra':
840 $slides_per_view = $l_col;
841 $slides_per_group = $l_group;
842
843 if ( empty( $el_breakpoints['laptop'] ) ) {
844 $slides_per_view = $d_col;
845 $slides_per_group = $meta['d_group'];
846 }
847
848 break;
849
850 case 'tablet':
851 $slides_per_view = $te_col;
852 $slides_per_group = $te_group;
853
854 if ( empty( $el_breakpoints['laptop'] ) && empty( $el_breakpoints['tablet_extra'] ) ) {
855 $slides_per_view = $d_col;
856 $slides_per_group = $meta['d_group'];
857 } elseif ( empty( $el_breakpoints['laptop'] ) && ! empty( $el_breakpoints['tablet_extra'] ) ) {
858 $slides_per_view = $te_col;
859 $slides_per_group = $te_group;
860 } elseif ( ! empty( $el_breakpoints['laptop'] ) && empty( $el_breakpoints['tablet_extra'] ) ) {
861 $slides_per_view = $l_col;
862 $slides_per_group = $l_group;
863 }
864
865 break;
866
867 case 'mobile_extra':
868 $slides_per_view = $t_col;
869 $slides_per_group = $meta['t_group'];
870 $dynamic_bullets = $has_dynamic_dots;
871
872 break;
873
874 case 'mobile':
875 $slides_per_view = $t_col;
876 $slides_per_group = $meta['t_group'];
877 $dynamic_bullets = $has_dynamic_dots;
878
879 if ( ! empty( $el_breakpoints['mobile_extra'] ) ) {
880 $slides_per_view = $me_col;
881 $slides_per_group = $me_group;
882 }
883
884 break;
885 }
886
887 if ( $value['is_enabled'] ) {
888 $br = 'widescreen' !== $el_breakpoint ? $breakpoint_value + 1 : $breakpoint_value;
889
890 $breakpoints[ absint( $br ) ] = [
891 'slidesPerView' => absint( $slides_per_view ),
892 'slidesPerGroup' => absint( $slides_per_group ),
893 'pagination' => [
894 'dynamicBullets' => $dynamic_bullets,
895 ],
896 ];
897 }
898 }
899 } else {
900 $breakpoints = [
901 0 => [
902 'slidesPerView' => absint( $m_col ),
903 'slidesPerGroup' => absint( $meta['m_group'] ),
904 'pagination' => [
905 'dynamicBullets' => $has_dynamic_dots,
906 ],
907 ],
908 768 => [
909 'slidesPerView' => absint( $t_col ),
910 'slidesPerGroup' => absint( $meta['t_group'] ),
911 'pagination' => [
912 'dynamicBullets' => $has_dynamic_dots,
913 ],
914 ],
915 1025 => [
916 'slidesPerView' => absint( $d_col ),
917 'slidesPerGroup' => absint( $meta['d_group'] ),
918 'pagination' => [
919 'dynamicBullets' => $has_dynamic_dots,
920 ],
921 ],
922 ];
923 }
924
925 $slider_options = [
926 'slidesPerView' => absint( $d_col ),
927 'slidesPerGroup' => absint( $meta['d_group'] ),
928 'speed' => absint( $meta['speed'] ),
929 'loop' => $meta['loop'],
930 'autoHeight' => $meta['auto_height'],
931 'preloadImages' => ! $meta['lazy_load'],
932 'lazy' => $meta['lazy_load'],
933 'breakpoints' => $breakpoints,
934 ];
935
936 if ( ! empty( $meta['raw_settings']['slider_slide_animation'] ) ) {
937 $slider_options['watchSlidesProgress'] = true;
938 }
939 if ( $meta['auto_play'] ) {
940 $slider_options['autoplay'] = [
941 'delay' => absint( $meta['auto_play_timeout'] ),
942 'pauseOnMouseEnter' => $meta['stop_on_hover'],
943 'disableOnInteraction' => false,
944 ];
945 }
946
947 $slider_options = apply_filters( 'rtsb/elementor/render/slider_dataset', $slider_options, $meta );
948 $carouselClass = 'rtsb-carousel-slider slider-loading rtsb-pos-s ' . $meta['nav_position'] . '-nav' . $has_dots;
949
950 return [
951 'data' => esc_js( wp_json_encode( $slider_options ) ),
952 'class' => $carouselClass,
953 ];
954 }
955
956 /**
957 * Preloader.
958 *
959 * @param string $layout Layout.
960 *
961 * @return string
962 */
963 public static function pre_loader( $layout ) {
964 $loader_class = '';
965
966 $is_carousel = preg_match( '/slider/', $layout );
967
968 if ( $is_carousel ) {
969 $loader_class = ' full-op';
970 }
971 if ( ! Fns::enable_loader() ) {
972 return '';
973 }
974 return '<div class="rtsb-elements-loading rtsb-ball-clip-rotate"><div></div></div>';
975 }
976
977 /**
978 * Badge class.
979 *
980 * @param string $preset Badge preset.
981 *
982 * @return string|null
983 */
984 public static function badge_class( $preset ) {
985 switch ( $preset ) {
986 case 'preset-default':
987 $class = 'default-badge';
988 break;
989 case 'preset2':
990 $class = 'fill angle-right';
991 break;
992 case 'preset3':
993 $class = 'outline';
994 break;
995
996 case 'preset4':
997 $class = 'outline angle-right';
998 break;
999
1000 default:
1001 $class = 'fill';
1002 break;
1003 }
1004
1005 return $class;
1006 }
1007
1008 /**
1009 * Registers required scripts.
1010 *
1011 * @param array $scripts Scripts to register.
1012 *
1013 * @return void
1014 */
1015 public static function register_scripts( $scripts ) {
1016 $caro = false;
1017 $masonry = false;
1018 $script = [];
1019
1020 $script[] = 'jquery';
1021
1022 foreach ( $scripts as $sc => $value ) {
1023 if ( ! empty( $sc ) ) {
1024 if ( 'isCarousel' === $sc ) {
1025 $caro = $value;
1026 }
1027
1028 if ( 'isMasonry' === $sc ) {
1029 $masonry = $value;
1030 }
1031 }
1032 }
1033
1034 if ( count( $scripts ) ) {
1035 /**
1036 * Scripts.
1037 */
1038 if ( $caro ) {
1039 $script[] = 'swiper';
1040 }
1041
1042 if ( $masonry ) {
1043 $script[] = 'masonry';
1044 }
1045
1046 $script[] = 'imagesloaded';
1047 $script[] = Fns::optimized_handle( 'rtsb-public' );
1048
1049 foreach ( $script as $sc ) {
1050 wp_enqueue_script( $sc );
1051 }
1052 }
1053 }
1054
1055 /**
1056 * Check if a wrapper is needed based on the current theme.
1057 *
1058 * @return bool
1059 */
1060 public static function is_wrapper_needed() {
1061 $theme_list = apply_filters(
1062 'rtsb/products/inner/wrapper/basedon/themes',
1063 [
1064 'twentytwentyone',
1065 'twentytwentytwo',
1066 'twentytwentythree',
1067 'storefront',
1068 'hello-elementor',
1069 'astra',
1070 ],
1071 rtsb()->current_theme
1072 );
1073
1074 if ( in_array( rtsb()->current_theme, $theme_list, true ) ) {
1075 return true;
1076 }
1077
1078 return false;
1079 }
1080
1081 /**
1082 * Get the number of products per page based on WooCommerce settings.
1083 *
1084 * @return int
1085 */
1086 public static function get_products_per_page() {
1087 $products_row = absint( get_option( 'woocommerce_catalog_rows', 4 ) );
1088 $products_col = absint( get_option( 'woocommerce_catalog_columns', 4 ) );
1089 $products_per_page = apply_filters( 'rtsb/elementor/archive/products_per_page', $products_row * $products_col );
1090
1091 return ! empty( $products_per_page ) ? $products_per_page : 12;
1092 }
1093 /**
1094 * Render Filters View.
1095 *
1096 * @param array $templates Template name.
1097 * @param array $settings Control settings.
1098 *
1099 * @return string
1100 */
1101 public static function filters_view( $templates, $settings ) {
1102 global $wp;
1103 $filters = [];
1104 $html = null;
1105 $reset = ! empty( $settings['reset_btn'] );
1106 $reset_mode = 'show' === $settings['reset_btn_behavior'] ? ' show-reset' : ' ondemand-reset';
1107 $reset_text = ! empty( $settings['reset_btn_text'] ) ? $settings['reset_btn_text'] : esc_html__( 'Reset Filter', 'shopbuilder' );
1108 $scroll_mode = ! empty( $settings['enable_scroll'] ) ? ' has-scroll' : ' no-scroll';
1109 $scroll_height = ! empty( $settings['enable_scroll'] ) ? $settings['scroll_height']['size'] : 300;
1110 $has_mobile_toggle = ! empty( $settings['filter_mobile_toggle'] );
1111 $toggle_class = $has_mobile_toggle ? ' default-filter-has-toggle' : ' default-filter-no-toggle';
1112 $mobile_toggle_text = ! empty( $settings['filter_mobile_toggle_text'] ) ? $settings['filter_mobile_toggle_text'] : esc_html__( 'Click to Filter', 'shopbuilder' );
1113 $scroll_attr = '';
1114
1115 if ( ! empty( $settings['enable_scroll'] ) ) {
1116 $scroll_attr = ' style="--rtsb-filter-scroll-height: ' . absint( $scroll_height ) . 'px;"';
1117 }
1118
1119 foreach ( $settings['filter_types'] as $key => $item ) {
1120 if ( ! defined( 'RTWPVS_PLUGIN_FILE' ) && in_array( $item['input_type_all'], [ 'color', 'button', 'image' ], true ) ) {
1121 $item['input_type_all'] = 'checkbox';
1122 }
1123 $filters[] = [
1124 'filter_title' => $item['filter_title'],
1125 'filter_items' => $item['filter_items'],
1126 'filter_attr' => $item['filter_attr'],
1127 'input_type' => 'product_attr' === $item['filter_items'] ? $item['input_type_all'] : $item['input_type'],
1128 ];
1129
1130 if ( 'rating_filter' === $item['filter_items'] ) {
1131 $filters[ $key ]['template'] = $templates['rating'];
1132 } elseif ( 'price_filter' === $item['filter_items'] ) {
1133
1134 $filters[ $key ]['input_type'] = 'slider';
1135 $filters[ $key ]['template'] = $templates['price'];
1136 } else {
1137 $filters[ $key ]['template'] = 'product_attr' === $item['filter_items'] ? $templates[ $item['input_type_all'] ] : $templates[ $item['input_type'] ];
1138 }
1139 }
1140
1141 if ( '' === get_option( 'permalink_structure' ) ) {
1142 $form_action = remove_query_arg( [ 'page', 'paged', 'product-page' ], add_query_arg( $wp->query_string, '', home_url( $wp->request ) ) );
1143 } else {
1144 $form_action = preg_replace( '%\/page/[0-9]+%', '', home_url( trailingslashit( $wp->request ) ) );
1145 }
1146 $args = [
1147 'filters' => $filters,
1148 'scroll_mode' => $scroll_mode,
1149 'reset_mode' => $reset_mode,
1150 'reset' => $reset,
1151 'scroll_attr' => $scroll_attr,
1152 'toggle_class' => $toggle_class,
1153 'reset_text' => $reset_text,
1154 'settings' => $settings,
1155 'form_action' => $form_action,
1156 ];
1157
1158 $html .= '<div class="rtsb-default-archive-filters' . esc_attr( $toggle_class ) . '">';
1159
1160 if ( $has_mobile_toggle ) {
1161 $html .= '<div class="rtsb-filter-mobile-toggle">
1162 <button class="product-filter-toggle">
1163 <span class="icon"><i aria-hidden="true" class="toggle-icon rtsb-icon rtsb-icon-filter"></i></span>
1164 <span class="text reset-text">' . esc_html( $mobile_toggle_text ) . '</span>
1165 <span></span>
1166 </button>
1167 </div>';
1168 }
1169
1170 $html .= Fns::load_template( $templates['layout'], $args, true );
1171 $html .= '</div>';
1172 return $html;
1173 }
1174 /**
1175 * Get taxonomy type.
1176 *
1177 * @param string $tax_type Taxonomy type.
1178 * @param string $attr_type Attribute type.
1179 *
1180 * @return string
1181 */
1182 public static function get_product_filters_tax_type( $tax_type, $attr_type = '' ) {
1183 if ( 'product_attr' !== $tax_type ) {
1184 return $tax_type;
1185 }
1186
1187 if ( empty( $attr_type ) ) {
1188 return $tax_type;
1189 }
1190
1191 return $attr_type;
1192 }
1193 /**
1194 * Get attribute terms.
1195 *
1196 * @param string $tax Taxonomy type.
1197 * @return int[]|string|string[]|\WP_Error|\WP_Term[]
1198 */
1199 public static function get_product_filters_attribute_terms( $tax ) {
1200 $cache_key = 'get_default_filter_attribute_terms_' . $tax;
1201 if ( isset( self::$cache[ $cache_key ] ) ) {
1202 return self::$cache[ $cache_key ];
1203 }
1204
1205 $attributes = wc_get_attribute_taxonomies();
1206 $att_name = str_replace( 'pa_', '', $tax );
1207 $exist = array_search( $att_name, array_column( $attributes, 'attribute_name' ), true );
1208 if ( false === $exist ) {
1209 self::$cache[ $cache_key ] = [];
1210 return self::$cache[ $cache_key ];
1211 }
1212 $attribute = $tax;
1213
1214 if ( ! is_shop() && is_product_taxonomy() ) {
1215 $term = get_queried_object();
1216
1217 if ( ! $term instanceof WP_Term ) {
1218 return [];
1219 }
1220
1221 $args = [
1222 'status' => 'publish',
1223 'limit' => -1,
1224 'return' => 'ids',
1225 ];
1226
1227 if ( 'product_cat' === $term->taxonomy ) {
1228 $args['category'] = [ $term->slug ];
1229 }
1230
1231 if ( 'product_tag' === $term->taxonomy ) {
1232 $args['tag'] = [ $term->slug ];
1233 }
1234
1235 if ( strpos( $term->taxonomy, 'pa_' ) !== false ) {
1236 $args['tax_query'] = [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
1237 [
1238 'taxonomy' => $term->taxonomy,
1239 'field' => 'term_id',
1240 'terms' => $term->term_id,
1241 'operator' => 'IN',
1242 ],
1243 ];
1244 }
1245
1246 $query = new WC_Product_Query( $args );
1247 $products = $query->get_products();
1248 $attr_terms = [];
1249
1250 foreach ( $products as $product_id ) {
1251 $product_attr = wp_get_post_terms( $product_id, $attribute );
1252
1253 foreach ( $product_attr as $attr ) {
1254 $attr_terms[] = $attr;
1255 }
1256 }
1257 self::$cache[ $cache_key ] = self::filter_products_array_unique( $attr_terms );
1258 return self::$cache[ $cache_key ];
1259 } else {
1260 self::$cache[ $cache_key ] = get_terms(
1261 [
1262 'taxonomy' => $attribute,
1263 'hide_empty' => false,
1264 ]
1265 );
1266 return self::$cache[ $cache_key ];
1267 }
1268 }
1269 /**
1270 * Remove duplicate values from an array.
1271 *
1272 * @param array $array The input array.
1273 * @param bool $keep_key_assoc Whether to keep the key associations after removing duplicates.
1274 * @return array The array with duplicates removed.
1275 */
1276 public static function filter_products_array_unique( $array, $keep_key_assoc = false ) {
1277 $duplicate_keys = [];
1278 $tmp = [];
1279
1280 foreach ( $array as $key => $val ) {
1281 // convert objects to arrays, in_array() does not support objects.
1282 if ( is_object( $val ) ) {
1283 $val = (array) $val;
1284 }
1285
1286 if ( ! in_array( $val, $tmp, true ) ) {
1287 $tmp[] = $val;
1288 } else {
1289 $duplicate_keys[] = $key;
1290 }
1291 }
1292
1293 foreach ( $duplicate_keys as $key ) {
1294 unset( $array[ $key ] );
1295 }
1296
1297 return $keep_key_assoc ? $array : array_values( $array );
1298 }
1299 /**
1300 * Retrieve product categories (including hierarchical support)
1301 *
1302 * @param string $taxonomy Taxonomy name.
1303 * @return int[]|string|string[]|\WP_Error|\WP_Term[]
1304 */
1305 public static function get_all_terms( $taxonomy ) {
1306 $args = [
1307 'taxonomy' => $taxonomy,
1308 'suppress_filter' => true,
1309 'hide_empty' => false,
1310 'pad_counts' => true,
1311 'hierarchical' => true,
1312 ];
1313
1314 // Archive-scope only when the URL path itself is a term archive
1315 // (e.g. /product-category/clothing/). On the shop page WordPress can
1316 // resolve ?product_cat=X to a queried term, but in that case we want
1317 // the full hierarchy to remain visible for additional filtering.
1318 $is_term_archive_url = self::request_path_is_term_archive( $taxonomy );
1319
1320 $cache_key = $is_term_archive_url
1321 ? 'rtsb_get_terms_filter_products_archive_' . $taxonomy
1322 : 'rtsb_get_terms_filter_products_full_' . $taxonomy;
1323 $taxonomy_terms = wp_cache_get( $cache_key, 'shopbuilder' );
1324
1325 if ( false === $taxonomy_terms ) {
1326 $archive_term = $is_term_archive_url ? self::resolve_archive_term_from_path( $taxonomy ) : null;
1327
1328 if ( $archive_term instanceof WP_Term ) {
1329 $term_id = $archive_term->term_id;
1330 $archive_cache_key = 'rtsb_get_terms_filter_products_' . $taxonomy . $term_id;
1331 $taxonomy_terms = wp_cache_get( $archive_cache_key, 'shopbuilder' );
1332 if ( false === $taxonomy_terms ) {
1333 // Treat the URL-derived term as the root of the visible
1334 // hierarchy so the recursive renderer in
1335 // get_product_default_filter_list_html() (which starts
1336 // with $parent = 0) picks it up on child-category
1337 // archives. Clone first to avoid mutating WP's term
1338 // object.
1339 $current_term = clone $archive_term;
1340 $current_term->parent = 0;
1341 $taxonomy_terms = [ $current_term ];
1342 $child_args = [
1343 'taxonomy' => $taxonomy,
1344 'child_of' => $term_id,
1345 'hide_empty' => false,
1346 ];
1347
1348 $child_terms = get_terms( $child_args );
1349
1350 if ( ! empty( $child_terms ) ) {
1351 $taxonomy_terms = array_merge( $taxonomy_terms, $child_terms );
1352 }
1353
1354 wp_cache_set( $archive_cache_key, $taxonomy_terms, 'shopbuilder' );
1355 Cache::set_data_cache_key( $archive_cache_key );
1356 }
1357 } else {
1358 $taxonomy_terms = get_terms( $args );
1359 }
1360
1361 wp_cache_set( $cache_key, $taxonomy_terms, 'shopbuilder' );
1362 Cache::set_data_cache_key( $cache_key );
1363 }
1364
1365 return $taxonomy_terms;
1366 }
1367
1368 /**
1369 * Resolve the archive term from the request URL path. Avoids relying on
1370 * get_queried_object() which can return an unexpected term when
1371 * ?product_cat=a,b,c is also present in the request.
1372 *
1373 * @param string $taxonomy Taxonomy slug.
1374 *
1375 * @return \WP_Term|null
1376 */
1377 private static function resolve_archive_term_from_path( $taxonomy ) {
1378 $rewrite_slugs = self::get_taxonomy_archive_slugs( $taxonomy );
1379
1380 if ( empty( $rewrite_slugs ) ) {
1381 return null;
1382 }
1383
1384 // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Path only used for slug extraction.
1385 $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '';
1386 // phpcs:enable
1387 $path = (string) wp_parse_url( $request_uri, PHP_URL_PATH );
1388
1389 if ( '' === $path ) {
1390 return null;
1391 }
1392
1393 foreach ( $rewrite_slugs as $base ) {
1394 $base = trim( $base, '/' );
1395 $needle = '/' . $base . '/';
1396
1397 $pos = strpos( $path, $needle );
1398 if ( false === $pos ) {
1399 continue;
1400 }
1401
1402 $remainder = substr( $path, $pos + strlen( $needle ) );
1403 $segments = array_values(
1404 array_filter(
1405 explode( '/', $remainder ),
1406 static function ( $segment ) {
1407 return '' !== $segment;
1408 }
1409 )
1410 );
1411
1412 if ( empty( $segments ) ) {
1413 continue;
1414 }
1415
1416 $slug = sanitize_title( end( $segments ) );
1417 $term = get_term_by( 'slug', $slug, $taxonomy );
1418
1419 if ( $term instanceof WP_Term ) {
1420 return $term;
1421 }
1422 }
1423
1424 return null;
1425 }
1426
1427 /**
1428 * Decide whether the current REQUEST_URI path represents a real term
1429 * archive page for the given taxonomy. URL-based so it ignores cases
1430 * where WordPress merely resolves a query string into a queried term
1431 * on the shop page.
1432 *
1433 * @param string $taxonomy Taxonomy slug.
1434 *
1435 * @return bool
1436 */
1437 private static function request_path_is_term_archive( $taxonomy ) {
1438 $rewrite_slugs = self::get_taxonomy_archive_slugs( $taxonomy );
1439
1440 if ( empty( $rewrite_slugs ) ) {
1441 return false;
1442 }
1443
1444 // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Path only used in match.
1445 $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '';
1446 // phpcs:enable
1447 $path = (string) wp_parse_url( $request_uri, PHP_URL_PATH );
1448
1449 if ( '' === $path ) {
1450 return false;
1451 }
1452
1453 foreach ( $rewrite_slugs as $slug ) {
1454 if ( '' === $slug ) {
1455 continue;
1456 }
1457 if ( false !== strpos( $path, '/' . trim( $slug, '/' ) . '/' ) ) {
1458 return true;
1459 }
1460 }
1461
1462 return false;
1463 }
1464
1465 /**
1466 * Resolve the URL slug(s) used by a taxonomy archive (e.g. product_cat
1467 * maps to "product-category"). Falls back to common WooCommerce defaults
1468 * when the taxonomy object is unavailable.
1469 *
1470 * @param string $taxonomy Taxonomy slug.
1471 *
1472 * @return array<string>
1473 */
1474 private static function get_taxonomy_archive_slugs( $taxonomy ) {
1475 $slugs = [];
1476
1477 if ( taxonomy_exists( $taxonomy ) ) {
1478 $tax_obj = get_taxonomy( $taxonomy );
1479 if ( $tax_obj && ! empty( $tax_obj->rewrite['slug'] ) ) {
1480 $slugs[] = $tax_obj->rewrite['slug'];
1481 }
1482 }
1483
1484 $defaults = [
1485 'product_cat' => 'product-category',
1486 'product_tag' => 'product-tag',
1487 'product_brand' => 'product-brand',
1488 ];
1489
1490 if ( isset( $defaults[ $taxonomy ] ) ) {
1491 $slugs[] = $defaults[ $taxonomy ];
1492 }
1493
1494 return array_unique( array_filter( $slugs ) );
1495 }
1496 /**
1497 * Generates an error message block.
1498 *
1499 * @param array $title The title array of the error message block.
1500 * @param string $wrapper Additional CSS class for the wrapper div.
1501 *
1502 * @return string The HTML representation of the error message block.
1503 */
1504 public static function filter_error_message( $title, $wrapper ) {
1505 $html = '<div class="rtsb-product-default-filters ' . esc_attr( $wrapper ) . '">';
1506 $html .= self::get_default_filter_title( $title );
1507 $html .= '<div class="default-filter-content">';
1508 $html .= '<p class="no-filter">' . esc_html__( 'Nothing found.', 'shopbuilder' ) . '</p>';
1509 $html .= '</div>';
1510 $html .= '</div>';
1511
1512 return $html;
1513 }
1514 /**
1515 * Get filter title.
1516 *
1517 * @param array $title Filter title.
1518 * @return string
1519 */
1520 public static function get_default_filter_title( $title ) {
1521 $html = '';
1522
1523 if ( empty( $title['title'] ) ) {
1524 return $html;
1525 }
1526
1527 $html .= '<div class="default-filter-title-wrapper">';
1528
1529 $html .= '<h3 class="widget-title rtsb-d-flex rtsb-align-items-center"><span class="title">' . $title['title'] . '</span></h3>';
1530
1531 $html .= '</div>';
1532
1533 return apply_filters( 'rtsb/elementor/default/filter/title', $html, $title );
1534 }
1535 /**
1536 * Recursive function to generate the filter list with hierarchy.
1537 *
1538 * @param array $data Array of required data.
1539 * @param string $input Type of input field for the filter (e.g., checkbox).
1540 * @param array $additional_data Data for additional filtering.
1541 * @param int $parent ID of the parent term (default: 0).
1542 *
1543 * @return string The generated HTML for the product filter list.
1544 */
1545 public static function get_product_default_filter_list_html( $data, $input = 'checkbox', $additional_data = [], $parent = 0 ) {
1546
1547 $html = '';
1548 $default_tax = [];
1549 $is_attribute = ! empty( $data['is_attribute'] );
1550
1551 $param_key = isset( $additional_data['taxonomy'] ) ? $additional_data['taxonomy'] : '';
1552 if ( $param_key && 0 === strpos( $param_key, 'pa_' ) ) {
1553 $param_key = 'filter_' . substr( $param_key, 3 );
1554 }
1555
1556 // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1557 $raw_values = ( $param_key && isset( $_GET[ $param_key ] ) ) ? wc_clean( wp_unslash( $_GET[ $param_key ] ) ) : '';
1558 $active_option = $raw_values ? array_map( 'urldecode', explode( ',', $raw_values ) ) : [];
1559 $counter = 0;
1560
1561 if ( $is_attribute ) {
1562 list(
1563 'filter_name' => $filter_name,
1564 'base_link' => $base_link
1565 ) = self::get_product_filters_attribute_term_info( $additional_data['taxonomy'] );
1566 }
1567
1568 $html .= '<ul class="product-default-filters input-type-' . esc_attr( $input ) . '">';
1569
1570 foreach ( $data['taxonomy'] as $tax ) {
1571 if ( $tax->parent !== $parent ) {
1572 continue;
1573 }
1574
1575 if ( $is_attribute ) {
1576 $tax_link = remove_query_arg( $filter_name, $base_link );
1577 $tax_link = add_query_arg( $filter_name, $tax->slug, $tax_link );
1578 } else {
1579 $tax_link = get_term_link( $tax );
1580 }
1581
1582 $unique_id = substr( uniqid(), -4 );
1583
1584 $decoded_slug = urldecode( $tax->slug );
1585 $is_active = in_array( $decoded_slug, $active_option, true ) || in_array( $tax->slug, $active_option, true );
1586 $active_tax = $is_active ? ' checked' : '';
1587 $active_tax_parent = $is_active ? ' active' : '';
1588 $term_children = in_array( $tax->taxonomy, [ 'product_cat', 'product_brand' ], true ) ? get_term_children( $tax->term_id, $tax->taxonomy ) : [];
1589 $taxonomy = $tax->taxonomy;
1590 $product_count = self::get_visible_term_count( $tax->taxonomy, $tax->term_id );
1591
1592 // On a product taxonomy archive (category/tag/attribute page) scope the
1593 // count to the current archive - only products that are in THIS archive
1594 // AND have this term. This makes the displayed count reflect the current
1595 // category context and lets terms that are empty in this archive be
1596 // dropped from the list.
1597 if ( is_product_taxonomy() || 'archive' === apply_filters( 'rtsb/builder/set/current/page/type', '' ) ) {
1598 $product_count = self::count_tax_data( $tax, $product_count );
1599 }
1600
1601 // Show Empty Categories.
1602 if ( empty( $data['show_empty_terms'] ) && 0 === $product_count ) {
1603 continue;
1604 }
1605
1606 $html .= '<li class="rtsb-default-filter-term-item term-item-' . absint( $tax->term_id ) . esc_attr( $term_children ? ' term-has-children' : '' ) . ( ! $data['show_child'] ? ' child-terms-hidden' : '' ) . '">
1607 <div class="rtsb-default-filter-group' . esc_attr( $active_tax_parent ) . '">
1608 <input type="' . esc_attr( $input ) . '" class="rtsb-default-filter-trigger rtsb-' . esc_attr( $input . '-filter rtsb-term-' . $tax->slug . $active_tax ) . '" name="rtsb-filter-' . esc_attr( $taxonomy ) . '[]" id="rtsb-default-term-filter-' . $unique_id . absint( $tax->term_id ) . '" value="' . esc_attr( $tax->slug ) . '" ' . esc_attr( $active_tax ) . '>
1609 <label class="rtsb-default-' . esc_attr( $input ) . '-filter-label" for="rtsb-default-term-filter-' . $unique_id . absint( $tax->term_id ) . '">
1610 <span class="name">' . esc_html( $tax->name ) . '</span>
1611 </label>
1612 <div class="rtsb-product-count">(' . esc_html( $product_count ) . ')</div>
1613 ' . ( ! empty( $term_children ) && $data['show_child'] ? '<i class="rtsb-plus-icon"></i>' : '' ) . '
1614 </div>';
1615
1616 // Show Sub-categories.
1617 if ( $data['show_child'] ) {
1618 $children = self::get_product_default_filter_list_html( $data, $input, $additional_data, $tax->term_id );
1619
1620 if ( ! empty( $children ) ) {
1621 $html .= '<div class="filter-child">' . $children . '</div>';
1622 }
1623 }
1624
1625 $counter++;
1626
1627 $html .= '</li>';
1628 }
1629
1630 $html .= '</ul>';
1631
1632 // Check if the output contains any child elements.
1633 $has_children = strpos( $html, '<li class="rtsb-default-filter-term-item' ) !== false;
1634
1635 if ( ! $has_children ) {
1636 // Remove the outer <ul> if there are no child elements.
1637 $html = '';
1638 }
1639 return $html;
1640 }
1641 /**
1642 * Get attribute term info.
1643 *
1644 * @param string $tax Taxonomy type.
1645 * @return array
1646 */
1647 public static function get_product_filters_attribute_term_info( $tax ) {
1648 $result = [];
1649 $attributes = wc_get_attribute_taxonomies();
1650
1651 foreach ( $attributes as $attr ) {
1652 if ( str_replace( 'pa_', '', $tax ) === $attr->attribute_name ) {
1653 $term_type = $attr->attribute_type;
1654 $attribute_slug = $attr->attribute_name;
1655 $attribute = wc_attribute_taxonomy_name( $attr->attribute_name );
1656 break;
1657 }
1658 }
1659
1660 $filter_name = 'filter_' . wc_attribute_taxonomy_slug( $attribute_slug );
1661
1662 $result['attribute'] = $attribute ?? '';
1663 $result['term_type'] = $term_type ?? '';
1664 $result['attribute_slug'] = $attribute_slug ?? '';
1665 $result['filter_name'] = $filter_name;
1666 $result['base_link'] = self::get_base_url();
1667
1668 return $result;
1669 }
1670 /**
1671 * Get base URL.
1672 *
1673 * @return string|null
1674 */
1675 public static function get_base_url() {
1676 global $wp;
1677 return home_url( add_query_arg( [], $wp->request ) );
1678 }
1679 /**
1680 * Count the number of occurrences for a specific term in a taxonomy for the current queried object.
1681 *
1682 * @param object|array $tax The term object for the taxonomy.
1683 * @param int $count The initial count value.
1684 *
1685 * @return int The updated count value.
1686 */
1687 public static function count_tax_data( $tax, $count ) {
1688 $page = get_queried_object();
1689
1690 // Scoping only applies on a real taxonomy archive where the queried
1691 // object is a term. On other contexts (e.g. the shop page) the queried
1692 // object is not a term, so keep the original (global) count.
1693 if ( ! $page instanceof \WP_Term ) {
1694 return $count;
1695 }
1696
1697 if ( is_array( $tax ) && strpos( $tax['taxonomy'], 'attribute_' ) !== false ) {
1698 $taxonomy = str_replace( 'attribute_', '', $tax['taxonomy'] );
1699 $term_id = $tax['term_id'];
1700 } else {
1701 $taxonomy = $tax->taxonomy;
1702 $term_id = $tax->term_id;
1703 }
1704
1705 if ( strpos( $page->taxonomy, 'pa_' ) !== false ) {
1706 $args['status'] = 'publish';
1707 $args['limit'] = -1;
1708 $args['return'] = 'ids';
1709 $args['tax_query'] = [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
1710 'relation' => 'AND',
1711 [
1712 'taxonomy' => $page->taxonomy,
1713 'field' => 'term_id',
1714 'terms' => $page->term_id,
1715 'operator' => 'IN',
1716 ],
1717 [
1718 'taxonomy' => $tax->taxonomy,
1719 'field' => 'term_id',
1720 'terms' => $tax->term_id,
1721 'operator' => 'IN',
1722 ],
1723 [
1724 'taxonomy' => 'product_visibility',
1725 'terms' => [ 'exclude-from-catalog' ],
1726 'field' => 'name',
1727 'operator' => 'NOT IN',
1728 ],
1729 ];
1730
1731 $query = new WC_Product_Query( $args );
1732 $products = $query->get_products();
1733
1734 return count( $products );
1735 }
1736
1737 // Scope the count to the current archive term (category, brand, tag, etc.)
1738 // so it reflects only products that are in this archive AND also have the
1739 // filter term. Works for cross-taxonomy filters (e.g. brands on a tag
1740 // archive) and same-taxonomy filters (e.g. the Tags filter on a tag
1741 // archive shows the intersection with the current tag).
1742 $args = [
1743 'limit' => -1,
1744 'return' => 'ids',
1745 'status' => 'publish',
1746 'tax_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
1747 'relation' => 'AND',
1748 [
1749 'taxonomy' => $taxonomy,
1750 'field' => 'term_id',
1751 'terms' => $term_id,
1752 ],
1753 [
1754 'taxonomy' => $page->taxonomy,
1755 'field' => 'term_id',
1756 'terms' => $page->term_id,
1757 ],
1758 [
1759 'taxonomy' => 'product_visibility',
1760 'terms' => [ 'exclude-from-catalog' ],
1761 'field' => 'name',
1762 'operator' => 'NOT IN',
1763 ],
1764 ],
1765 ];
1766
1767 $query = new WC_Product_Query( $args );
1768 $products = $query->get_products();
1769
1770 return count( $products );
1771 }
1772
1773 /**
1774 * Get the count of visible (non-hidden) products for a given taxonomy term.
1775 *
1776 * Excludes products with WooCommerce visibility set to 'Hidden'
1777 * (products assigned the 'exclude-from-catalog' term).
1778 *
1779 * @param string $taxonomy The taxonomy name.
1780 * @param int $term_id The term ID.
1781 *
1782 * @return int Visible product count.
1783 */
1784 public static function get_visible_term_count( $taxonomy, $term_id ) {
1785 static $cache = [];
1786
1787 $cache_key = $taxonomy . '_' . $term_id;
1788
1789 if ( isset( $cache[ $cache_key ] ) ) {
1790 return $cache[ $cache_key ];
1791 }
1792
1793 $args = [
1794 'limit' => -1,
1795 'return' => 'ids',
1796 'status' => 'publish',
1797 'tax_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
1798 'relation' => 'AND',
1799 [
1800 'taxonomy' => $taxonomy,
1801 'field' => 'term_id',
1802 'terms' => $term_id,
1803 ],
1804 [
1805 'taxonomy' => 'product_visibility',
1806 'terms' => [ 'exclude-from-catalog' ],
1807 'field' => 'name',
1808 'operator' => 'NOT IN',
1809 ],
1810 ],
1811 ];
1812
1813 $query = new WC_Product_Query( $args );
1814 $count = count( $query->get_products() );
1815
1816 $cache[ $cache_key ] = $count;
1817
1818 return $count;
1819 }
1820
1821 /**
1822 * Get filter HTML.
1823 *
1824 * @param string $filter_type Filter type.
1825 * @param array $options Options.
1826 * @param array $additional_data Data for additional filtering.
1827 * @param string $input Input type.
1828 *
1829 * @return string The generated HTML for the attribute filter list.
1830 */
1831 public static function get_default_filter_html( $filter_type, $options, $additional_data = [], $input = 'checkbox' ) {
1832 $html = '<ul class="product-filters input-type-' . esc_attr( $input ) . '" ';
1833 $active_option = isset( $_GET[ $filter_type ] ) ? explode( ',', wc_clean( wp_unslash( $_GET[ $filter_type ] ) ) ) : []; // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1834 $option_link = self::get_base_url() . '/' . $filter_type . '/';
1835
1836 foreach ( $options as $option => $option_name ) {
1837 $unique_id = substr( uniqid(), -6 );
1838 $active_tax = in_array( $option, $active_option, true ) ? ' checked' : '';
1839 $active_group = in_array( $option, $active_option, true ) ? ' active' : '';
1840
1841 $html .= '
1842 <li class="rtsb-term-item">
1843 <div class="rtsb-default-filter-group' . esc_attr( $active_group ) . '">
1844 <input type="' . esc_attr( $input ) . '" class="rtsb-default-filter-trigger rtsb-' . esc_attr( $input ) . '-filter rtsb-term-' . esc_attr( $option ) . esc_attr( $active_tax ) . '" name="rtsb-filter-' . esc_attr( $filter_type ) . '[]" id="rtsb-term-default-filter-' . $unique_id . '" value="' . esc_attr( $option ) . '" ' . $active_tax . '>
1845 <label class="rtsb-default-' . esc_attr( $input ) . '-filter-label" for="rtsb-term-default-filter-' . $unique_id . '">
1846 <span class="name">' . esc_html( $option_name ) . '</span>
1847 </label>
1848 </div>
1849 </li>';
1850 }
1851
1852 $html .= '</ul>';
1853
1854 return $html;
1855 }
1856 /**
1857 * Get the count of products with a specific star rating.
1858 *
1859 * @param int $star_rating The star rating value.
1860 *
1861 * @return int
1862 */
1863 public static function get_product_rating_count( $star_rating ) {
1864 $args = [
1865 'status' => 'publish',
1866 'limit' => -1,
1867 'product_rating' => sprintf( '%.1f', $star_rating ),
1868 ];
1869
1870 if ( is_product_taxonomy() ) {
1871 $term = get_queried_object();
1872 $term_type = 'product_cat' === $term->taxonomy ? 'product_category_id' : 'product_tag_id';
1873
1874 if ( $term instanceof WP_Term ) {
1875 $args[ $term_type ] = [ $term->term_id ];
1876 }
1877 }
1878
1879 $query = new WC_Product_Query( $args );
1880 $products = $query->get_products();
1881
1882 return count( $products );
1883 }
1884 /**
1885 * Get attribute filter HTML.
1886 *
1887 * @param array $data Array of required data.
1888 * @param string $input Type of input field for the filter (e.g., color).
1889 *
1890 * @return string The generated HTML for the attribute filter list.
1891 */
1892 public static function get_attribute_filter_list_html( $data, $input = 'color' ) {
1893 if ( ! function_exists( 'rtwpvs' ) ) {
1894 return '';
1895 }
1896
1897 $terms = $data['terms'];
1898 $term_info = $data['term_info'];
1899 $show_tooltips = $data['show_tooltips'];
1900 $show_empty_terms = $data['show_empty_terms'];
1901 $counter = 0;
1902
1903 $html = '<ul class="product-default-filters rtsb-terms-wrapper ' . esc_attr( $term_info['type'] ) . '-variable-wrapper' . esc_attr( $data['show_label'] ) . '">';
1904
1905 foreach ( $terms as $attr_term ) {
1906 $count = self::get_visible_term_count( $attr_term->taxonomy, $attr_term->term_id );
1907 $term_link = remove_query_arg( $term_info['filter_name'], $term_info['base_link'] );
1908 $term_link = add_query_arg( $term_info['filter_name'], $attr_term->slug, $term_link );
1909 $unique_id = substr( uniqid(), -6 );
1910 $name = 'term-' . wc_variation_attribute_name( $term_info['attribute'] ) . '-' . $unique_id;
1911 $decoded_slug = urldecode( $attr_term->slug );
1912 $is_selected = in_array( $decoded_slug, $term_info['current_filter'], true ) || in_array( $attr_term->slug, $term_info['current_filter'], true );
1913 $selected_class = $is_selected ? ' selected' : '';
1914 $selected_item = $is_selected ? ' checked' : '';
1915 $active_class = $is_selected ? ' active' : '';
1916
1917 $args = [
1918 'id' => $name,
1919 'name' => $attr_term->name,
1920 'link' => $term_link,
1921 'type' => $term_info['type'],
1922 'term_id' => $attr_term->term_id,
1923 'term_slug' => $attr_term->slug,
1924 'taxonomy' => wc_variation_attribute_name( $term_info['attribute'] ),
1925 'tooltips' => $show_tooltips,
1926 'attribute_name' => $term_info['attribute'],
1927 'selected_item' => $selected_item,
1928 ];
1929
1930 if ( 'archive' === apply_filters( 'rtsb/builder/set/current/page/type', '' ) && ! empty( $data['count_display'] ) && 'block' === $data['count_display'] ) {
1931 $count = self::count_tax_data( $args, $count );
1932 }
1933
1934 $args['count'] = $count;
1935
1936 if ( ! $show_empty_terms && 0 === $count ) {
1937 continue;
1938 }
1939
1940 $html .= '<li class="rtsb-default-filter-term-item term-item-' . esc_attr( $attr_term->term_id ) . ' rtsb-term rtsb-default-' . esc_attr( $input ) . '-term ' . esc_attr( $term_info['type'] ) . '-variable-term-' . esc_attr( $attr_term->slug ) . esc_attr( $selected_class ) . '">';
1941 $html .= '<div class="rtsb-default-filter-group' . esc_attr( $active_class ) . '">';
1942 $html .= self::get_input_type_html( $input, $args );
1943
1944 $counter++;
1945
1946 $html .= '</div>';
1947 $html .= '</li>';
1948 }
1949
1950 $html .= '</ul>';
1951
1952 return $html;
1953 }
1954 /**
1955 * Get input type HTML.
1956 *
1957 * @param string $input Input type.
1958 * @param array $args Args.
1959 * @return mixed|string|null
1960 */
1961 public static function get_input_type_html( $input, $args ) {
1962 $html = '';
1963 $count = $args['count'];
1964
1965 switch ( $input ) {
1966 case 'color':
1967 $color = sanitize_hex_color( get_term_meta( $args['term_id'], 'product_attribute_color', true ) );
1968
1969 $html .= sprintf(
1970 // phpcs:ignore Generic.Strings.UnnecessaryStringConcat.Found
1971 '<input type="checkbox" id="' . esc_attr( $args['id'] ) . '" value="' . esc_attr( $args['term_slug'] ) . '" class="rtsb-attr-hidden-field" name="rtsb-filter-' . esc_attr( $args['attribute_name'] ) . '[]' . '" ' . esc_attr( $args['selected_item'] ) . '/><label for="%s" title="' . esc_attr( $args['name'] ) . '" class="rtsb-default-filter-trigger rtsb-attr-filter rtsb-color-filter rtsb-term-span rtsb-term-span-%s%s"><span class="default-filter-attr-color" style="background-color:%s;"></span><span class="default-filter-attr-name">%s</span></label><div class="rtsb-count">(%s)</div>',
1972 esc_attr( $args['id'] ),
1973 esc_attr( $args['type'] ),
1974 $args['tooltips'] ? esc_attr( ' tipsy' ) : '',
1975 esc_attr( $color ),
1976 esc_html( $args['name'] ),
1977 absint( $count )
1978 );
1979 break;
1980
1981 case 'image':
1982 if ( ! function_exists( 'rtwpvs' ) ) {
1983 return $html;
1984 }
1985
1986 $attachment_id = absint( get_term_meta( $args['term_id'], 'product_attribute_image', true ) );
1987 $image_size = rtwpvs()->get_option( 'attribute_image_size' );
1988 $image_url = wp_get_attachment_image_url( $attachment_id, apply_filters( 'rtsb_product_attribute_image_size', $image_size ) );
1989
1990 $html .= sprintf(
1991 // phpcs:ignore Generic.Strings.UnnecessaryStringConcat.Found
1992 '<input type="checkbox" id="' . esc_attr( $args['id'] ) . '" value="' . esc_attr( $args['term_slug'] ) . '" class="rtsb-attr-hidden-field" name="rtsb-filter-' . esc_attr( $args['attribute_name'] ) . '[]' . '" ' . esc_attr( $args['selected_item'] ) . '/><label for="%s" title="' . esc_attr( $args['name'] ) . '" class="rtsb-default-filter-trigger rtsb-image-filter rtsb-term-span rtsb-term-span-%s%s"><img class="rtsb-default-attr-filter" alt="%s" src="%s" /></label>',
1993 esc_attr( $args['id'] ),
1994 esc_attr( $args['type'] ),
1995 $args['tooltips'] ? esc_attr( ' tipsy' ) : '',
1996 esc_attr( $args['name'] ),
1997 esc_url( $image_url )
1998 );
1999 break;
2000
2001 case 'button':
2002 $html .= sprintf(
2003 // phpcs:ignore Generic.Strings.UnnecessaryStringConcat.Found
2004 '<input id="' . esc_attr( $args['id'] ) . '" type="checkbox" value="' . esc_attr( $args['term_slug'] ) . '" class="rtsb-attr-hidden-field" name="rtsb-filter-' . esc_attr( $args['attribute_name'] ) . '[]' . '" ' . esc_attr( $args['selected_item'] ) . '/><label for="%s" title="' . esc_attr( $args['name'] ) . '" class="rtsb-default-filter-trigger rtsb-attr-filter rtsb-button-filter rtsb-term-span rtsb-term-span-%s%s"><span class="default-filter-attr-name">%s</span><div class="rtsb-count">(%s)</div></label>',
2005 esc_attr( $args['id'] ),
2006 esc_attr( $args['type'] ),
2007 $args['tooltips'] ? esc_attr( ' tipsy' ) : '',
2008 esc_html( $args['name'] ),
2009 absint( $count )
2010 );
2011 break;
2012
2013 default:
2014 }
2015
2016 return apply_filters( 'rtsb/elementor/default/filter/get_input_type_html', $html );
2017 }
2018 /**
2019 * Get reset button.
2020 *
2021 * @param string $btn_text Button text.
2022 * @param array $settings Settings array.
2023 *
2024 * @return string
2025 */
2026 public static function get_default_filter_reset_button( $btn_text, $settings ) {
2027 $html = '';
2028
2029 ob_start();
2030
2031 $is_editor = ! empty( \Elementor\Plugin::$instance->editor ) && \Elementor\Plugin::$instance->editor->is_edit_mode();
2032 $active = ( $is_editor || ! empty( $_SERVER['QUERY_STRING'] ) ) ? ' active' : '';
2033 $reset = ! empty( $settings['reset_btn'] );
2034 $apply_text = ! empty( $settings['apply_filters_btn_text'] ) ? $settings['apply_filters_btn_text'] : esc_html__( 'Apply Filters', 'shopbuilder' );
2035 $apply_icon = ! empty( $settings['apply_filters_btn_icon'] ) ? $settings['apply_filters_btn_icon'] : [];
2036 echo '<div class="default-filter-btn-wrapper">';
2037 ?>
2038 <?php if ( $settings['show_filter_btn'] ) { ?>
2039 <div class="rtsb-product-default-filters rtsb-apply-filters-btn">
2040 <div class="reset-btn-item">
2041 <button class="rtsb-apply-filters init">
2042 <span class="icon"><?php Fns::print_html( Fns::icons_manager( $apply_icon, 'icon-default' ) ); ?></span>
2043 <span class="text reset-text"><?php echo esc_html( $apply_text ); ?></span>
2044 <span></span>
2045 </button>
2046 </div>
2047 </div>
2048 <?php } ?>
2049 <?php
2050 if ( $reset ) {
2051 ?>
2052 <div class="rtsb-product-default-filters rtsb-reset<?php echo esc_attr( $active ); ?>">
2053 <div class="reset-btn-item">
2054 <button class="product-default-filter-reset">
2055 <span class="text reset-text"><?php echo esc_html( $btn_text ); ?></span>
2056 <span></span>
2057 </button>
2058 </div>
2059 </div>
2060 <?php
2061 }
2062 echo '</div>';
2063
2064 $html .= ob_get_clean();
2065
2066 return $html;
2067 }
2068 }
2069