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
← All changes | app/Controllers/BuilderController.php +187 -90 2.1.63.4.2 View file →
@@ -8,14 +8,14 @@
8 8 namespace RadiusTheme\SB\Controllers;
9 9
10 10 use RadiusTheme\SB\Helpers\Fns;
11 11 use RadiusTheme\SB\Helpers\BuilderFns;
12 +use RadiusTheme\SB\Models\ElementList;
12 13 use RadiusTheme\SB\Traits\SingletonTrait;
13 14 use RadiusTheme\SB\Controllers\Builder\BuilderCpt;
14 15 use RadiusTheme\SB\Controllers\Hooks\BuilderHooks;
15 16 use RadiusTheme\SB\Elementor\Controls\ImageSelectorControl;
16 17 use RadiusTheme\SB\Elementor\Controls\Select2AjaxControl;
17 -use RadiusTheme\SB\Models\ElementList;
18 18
19 19 // Do not allow directly accessing this file.
20 20 if ( ! defined( 'ABSPATH' ) ) {
21 21 exit( 'This script cannot be accessed directly.' );
@@ -54,28 +54,24 @@
54 54 */
55 55 private function __construct() {
56 56 $this->builder_page_id = BuilderFns::builder_page_id_by_page();
57 57 $this->page_edit_with = Fns::page_edit_with( $this->builder_page_id );
58 -
59 58 if ( ! is_admin() ) {
60 59 BuilderHooks::instance();
61 60 }
62 61
63 62 BuilderCpt::instance();
64 -
65 - $this->elementor_init();
66 - $this->both_builder_frontend();
67 -
68 - // add_action( 'elementor/init', [ $this, 'elementor_init' ] );
69 - add_action( 'elementor/editor/before_enqueue_scripts', [ $this, 'editor_scripts' ] );
70 - // add_action( 'template_redirect', [ $this, 'both_builder_frontend' ], 99 );
71 -
63 + if ( defined( 'ELEMENTOR_VERSION' ) ) {
64 + $this->elementor_init();
65 + $this->both_builder_frontend();
66 + add_action( 'elementor/editor/before_enqueue_scripts', [ $this, 'editor_scripts' ] );
67 + }
72 68 // RT Select2 Ajax.
73 - add_action( 'wp_ajax_rt_select2_object_search', [ $this, 'select2_ajax_posts_filter_autocomplete' ] );
74 - add_action( 'wp_ajax_nopriv_rt_select2_object_search', [ $this, 'select2_ajax_posts_filter_autocomplete' ] );
69 + add_action( 'wp_ajax_rtsb_select2_object_search', [ $this, 'select2_ajax_posts_filter_autocomplete' ] );
70 + add_action( 'wp_ajax_nopriv_rtsb_select2_object_search', [ $this, 'select2_ajax_posts_filter_autocomplete' ] );
75 71 // Select2 ajax save data.
76 - add_action( 'wp_ajax_rt_select2_get_title', [ $this, 'select2_ajax_get_posts_value_titles' ] );
77 - add_action( 'wp_ajax_nopriv_rt_select2_get_title', [ $this, 'select2_ajax_get_posts_value_titles' ] );
72 + add_action( 'wp_ajax_rtsb_select2_get_title', [ $this, 'select2_ajax_get_posts_value_titles' ] );
73 + add_action( 'wp_ajax_nopriv_rtsb_select2_get_title', [ $this, 'select2_ajax_get_posts_value_titles' ] );
78 74 }
79 75
80 76 /**
81 77 * Apply for frontend.
@@ -92,13 +88,20 @@
92 88 *
93 89 * @return void
94 90 */
95 91 public function elementor_init() {
96 - add_action( 'elementor/elements/categories_registered', [ $this, 'add_elementor_widget_categories' ] );
97 - add_action( 'elementor/widgets/register', [ $this, 'init_widgets' ] );
98 - add_action( 'elementor/controls/register', [ $this, 'init_controls' ] );
99 - add_filter( 'elementor/editor/localize_settings', [ $this, 'promotePremiumWidgets' ] );
100 - add_action( 'elementor/icons_manager/additional_tabs', [ $this, 'rtsb_icons' ] );
92 + add_action(
93 + 'init',
94 + function () {
95 + if ( Fns::should_load_elementor_scripts() ) {
96 + add_action( 'elementor/elements/categories_registered', [ $this, 'add_elementor_widget_categories' ] );
97 + add_action( 'elementor/widgets/register', [ $this, 'init_widgets' ] );
98 + add_action( 'elementor/controls/register', [ $this, 'init_controls' ] );
99 + add_filter( 'elementor/editor/localize_settings', [ $this, 'promotePremiumWidgets' ] );
100 + add_action( 'elementor/icons_manager/additional_tabs', [ $this, 'rtsb_icons' ] );
101 + }
102 + }
103 + );
101 104 }
102 105
103 106 /**
104 107 * Register Category
@@ -107,21 +110,29 @@
107 110 *
108 111 * @return void
109 112 */
110 113 public function add_elementor_widget_categories( $elements_manager ) {
111 - $type = BuilderFns::builder_type( get_the_ID() );
112 - $builder_type = ! empty( BuilderFns::builder_page_types()[ $type ] ) ? BuilderFns::builder_page_types()[ $type ] : '';
113 - $builder_type = str_replace( 'My Account - ', '', $builder_type );
114 - $categories['rtsb-shopbuilder'] = [
115 - 'title' => esc_html__( 'ShopBuilder', 'shopbuilder' ) . ' - ' . ucfirst( $builder_type ),
116 - 'icon' => 'fa fa-plug',
117 - ];
118 - $categories['rtsb-shopbuilder-general'] = [
119 - 'title' => esc_html__( 'Shopbuilder - General', 'shopbuilder' ),
120 - 'icon' => 'fa fa-plug',
121 - ];
114 + $categories = [];
115 + $builder_id = get_queried_object_id();
116 + $id = Fns::wpml_object_id( $builder_id, BuilderFns::$post_type_tb, 'default' );
117 + $type = BuilderFns::builder_type( $id );
122 118
123 - $categories = apply_filters( 'rtsb_elementor_widgets_category_lists', $categories );
119 + if ( $type && 'popup-builder' !== $type ) {
120 + $builder_type = ! empty( BuilderFns::builder_page_types()[ $type ] ) ? BuilderFns::builder_page_types()[ $type ] : '';
121 + $builder_type = str_replace( [ 'My Account - ', 'Endpoint: ' ], '', $builder_type );
122 + $categories['rtsb-shopbuilder'] = [
123 + 'title' => esc_html__( 'ShopBuilder', 'shopbuilder' ) . ' - ' . ucfirst( $builder_type ),
124 + 'icon' => 'fa fa-plug',
125 + ];
126 + }
127 + if ( 'quick-view' !== $type ) {
128 + $categories['rtsb-shopbuilder-general'] = [
129 + 'title' => esc_html__( 'Shopbuilder - General', 'shopbuilder' ),
130 + 'icon' => 'fa fa-plug',
131 + ];
132 + }
133 + $categories = apply_filters( 'rtsb_elementor_widgets_category_lists', $categories );
134 +
124 135 $other_categories = $elements_manager->get_categories();
125 136 $categories = array_merge(
126 137 array_slice( $other_categories, 0, 1 ),
127 138 $categories,
@@ -127,10 +138,10 @@
127 138 $categories,
128 139 array_slice( $other_categories, 1 )
129 140 );
130 141
131 - $set_categories = function ( $categories ) {
132 - $this->categories = $categories;
142 + $set_categories = function ( $categories ) use ( $elements_manager ) {
143 + $elements_manager->categories = $categories;
133 144 };
134 145
135 146 $set_categories->call( $elements_manager, $categories );
136 147 }
@@ -151,10 +162,14 @@
151 162
152 163 /**
153 164 * Init Widgets
154 165 *
155 - * Include widgets files and register them
166 + * Include widget files and register them
156 167 *
168 + * @param object $widgets_manager Widgets Manager.
169 + *
170 + * @return void
171 + *
157 172 * @since 1.0.0
158 173 */
159 174 public function init_widgets( $widgets_manager ) {
160 175
@@ -172,45 +187,24 @@
172 187 continue;
173 188 }
174 189
175 190 if ( ! empty( $element['category'] ) ) {
176 - $widget = false;
177 - $import = apply_filters( 'rtsb_import_status', false );
178 - switch ( $element['category'] ) {
179 - case 'product':
180 - if ( BuilderFns::is_product() || $import ) {
181 - $widget = new $element['base_class']();
182 - }
183 - break;
184 - case 'shop':
185 - if ( BuilderFns::is_archive() || BuilderFns::is_shop() || $import ) {
186 - $widget = new $element['base_class']();
187 - }
188 - break;
189 - case 'archive':
190 - if ( BuilderFns::is_archive() || $import ) {
191 - $widget = new $element['base_class']();
192 - }
193 - break;
194 - case 'cart':
195 - if ( BuilderFns::is_cart() || $import ) {
196 - $widget = new $element['base_class']();
197 - }
198 - break;
199 - case 'checkout':
200 - if ( BuilderFns::is_checkout() || $import ) {
201 - $widget = new $element['base_class']();
202 - }
203 - break;
204 - case 'general':
191 + try {
192 + $widget = false;
193 + $import = apply_filters( 'rtsb_import_status', false );
194 +
195 + if ( $this->should_register_widget( $element['category'], $import ) ) {
205 196 $widget = new $element['base_class']();
206 - break;
207 - default:
208 - }
209 - $widget = apply_filters( 'rtsb/element/list/init/widgets', $widget, $element );
197 + }
210 198
211 - if ( $widget && is_object( $widget ) ) {
212 - $widgets_manager->register( $widget );
199 + $widget = apply_filters( 'rtsb/element/list/init/widgets', $widget, $element );
200 +
201 + if ( $widget && is_object( $widget ) ) {
202 + $widgets_manager->register( $widget );
203 + }
204 + } catch ( \Throwable $e ) {
205 + error_log( 'ShopBuilder widget error (' . $element['base_class'] . '): ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
206 + continue;
213 207 }
214 208 }
215 209 }
216 210 }
@@ -215,8 +209,48 @@
215 209 }
216 210 }
217 211
218 212 /**
213 + * Determine whether a widget for the given category should be registered
214 + * in the current request context.
215 + *
216 + * @param string $category Element category.
217 + * @param bool $import Whether an import is in progress.
218 + *
219 + * @return bool
220 + */
221 + protected function should_register_widget( $category, $import ) {
222 + // On the frontend the Elementor Widgets Manager can initialise at an
223 + // unpredictable time — e.g. Elementor Pro's Theme Builder boots it on
224 + // template_redirect — when WooCommerce query conditionals such as
225 + // is_shop()/is_archive() are not yet reliable. Widget registration is
226 + // cached for the whole request, so gating it by those conditionals can
227 + // permanently skip ShopBuilder widgets, leaving the builder template to
228 + // render empty container shells. Register unconditionally on the
229 + // frontend; keep the per-category gate only for the editor widget panel.
230 + if ( ! is_admin() ) {
231 + return true;
232 + }
233 +
234 + switch ( $category ) {
235 + case 'product':
236 + return BuilderFns::is_product() || $import;
237 + case 'shop':
238 + return BuilderFns::is_archive() || BuilderFns::is_shop() || $import;
239 + case 'archive':
240 + return BuilderFns::is_archive() || $import;
241 + case 'cart':
242 + return BuilderFns::is_cart() || $import;
243 + case 'checkout':
244 + return BuilderFns::is_checkout() || $import;
245 + case 'general':
246 + return true;
247 + default:
248 + return false;
249 + }
250 + }
251 +
252 + /**
219 253 * Adding custom icons in Elementor
220 254 *
221 255 * @param array $tabs Tabs.
222 256 *
@@ -222,9 +256,9 @@
222 256 *
223 257 * @return array
224 258 */
225 259 public function rtsb_icons( $tabs = [] ) {
226 - $custom_icons = $this->get_icons();
260 + $custom_icons = Fns::get_custom_icon_names();
227 261
228 262 $tabs['shopbuilder-icons'] = [
229 263 'name' => 'rtsb-fonts',
230 264 'label' => esc_html__( 'ShopBuilder Icons', 'shopbuilder' ),
@@ -281,16 +315,34 @@
281 315 *
282 316 * @since 2.0.0
283 317 */
284 318 if ( did_action( 'elementor/loaded' ) && 'elementor' === $this->page_edit_with ) {
285 - do_action( 'elementor/page_templates/header-footer/before_content' );
319 + do_action( 'elementor/page_templates/header-footer/before_content' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
286 320 }
287 321
288 322 $parent_class = apply_filters( 'rtsb/builder/content/parent_class', [] );
289 323
290 324 if ( BuilderFns::is_product() ) {
291 - $product_id = Fns::get_prepared_product_id();
325 + $product_id = Fns::get_prepared_product_id();
326 +
327 + // Ensure the WordPress global $product is a valid WC_Product before
328 + // wc_get_product_class() fires the `woocommerce_post_class` filter.
329 + // Some themes (e.g. Blocksy) read the global $product directly inside
330 + // that filter and call methods like get_type() on it, which throws a
331 + // fatal error when the global is still an unresolved string.
332 + $_product = Fns::get_product();
333 +
292 334 $parent_class = wc_get_product_class( $parent_class, $product_id );
335 +
336 + if ( function_exists( 'rtwpvs' ) && $_product instanceof \WC_Product ) {
337 + if ( $_product->is_type( 'variable' ) ) {
338 + $parent_class[] = 'rtwpvs-product';
339 + $beside_label = function_exists( 'rtwpvsp' ) && rtwpvs()->get_option( 'attribute_on_click_behavior' );
340 + if ( $beside_label ) {
341 + $parent_class[] = 'rtwpvs-selected-term-beside-label';
342 + }
343 + }
344 + }
293 345 }
294 346
295 347 if ( BuilderFns::is_shop() ) {
296 348 $parent_class[] = 'shop';
@@ -311,10 +363,34 @@
311 363 if ( BuilderFns::is_checkout() ) {
312 364 if ( ! apply_filters( 'rtsb/multi/step/checkout/widget', true ) ) {
313 365 return;
314 366 }
367 +
368 + $checkout = WC()->checkout();
369 +
370 + // Temporarily remove callbacks that Elementor checkout
371 + // handles via separate widgets to prevent duplicates.
372 + remove_action( 'woocommerce_before_checkout_form', [ Hooks\ActionHooks::class, 'woocommerce_checkout_login_form' ], 10 );
373 + remove_action( 'woocommerce_before_checkout_form', [ Hooks\ActionHooks::class, 'woocommerce_checkout_coupon_form' ], 10 );
374 + remove_action( 'woocommerce_before_checkout_form', [ Fns::class, 'woocommerce_output_all_notices' ], 10 );
375 +
376 + do_action( 'woocommerce_before_checkout_form', $checkout );
377 +
378 + // Restore callbacks for non-Elementor contexts.
379 + add_action( 'woocommerce_before_checkout_form', [ Hooks\ActionHooks::class, 'woocommerce_checkout_login_form' ], 10 );
380 + add_action( 'woocommerce_before_checkout_form', [ Hooks\ActionHooks::class, 'woocommerce_checkout_coupon_form' ], 10 );
381 + add_action( 'woocommerce_before_checkout_form', [ Fns::class, 'woocommerce_output_all_notices' ], 10 );
382 +
383 + // If checkout registration is disabled and not logged in, the user cannot checkout.
384 + if ( ! $checkout->is_registration_enabled() && $checkout->is_registration_required() && ! is_user_logged_in() ) {
385 + echo esc_html( apply_filters( 'woocommerce_checkout_must_be_logged_in_message', __( 'You must be logged in to checkout.', 'shopbuilder' ) ) );
386 + return;
387 + }
315 388 ?>
316 - <form name="checkout" method="post" class="rtsb-woocommerce-checkout checkout" action="<?php echo esc_url( wc_get_checkout_url() ); ?>" enctype="multipart/form-data">
389 + <!--
390 + woocommerce-checkout added for fix issue with payment method ( Revolut Pay )
391 + -->
392 + <form name="checkout" method="post" class="rtsb-woocommerce-checkout woocommerce-checkout checkout" action="<?php echo esc_url( wc_get_checkout_url() ); ?>" enctype="multipart/form-data">
317 393 <?php
318 394 }
319 395 }
320 396
@@ -331,9 +407,9 @@
331 407 *
332 408 * @since 2.0.0
333 409 */
334 410 if ( did_action( 'elementor/loaded' ) && 'elementor' === $this->page_edit_with ) {
335 - do_action( 'elementor/page_templates/header-footer/after_content' );
411 + do_action( 'elementor/page_templates/header-footer/after_content' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
336 412 }
337 413
338 414 if ( BuilderFns::is_checkout() ) {
339 415 if ( ! apply_filters( 'rtsb/multi/step/checkout/widget', true ) ) {
@@ -341,8 +417,9 @@
341 417 }
342 418 ?>
343 419 </form>
344 420 <?php
421 + do_action( 'woocommerce_after_checkout_form', WC()->checkout() );
345 422 }
346 423 ?>
347 424 </div>
348 425 <!-- After Content end -->
@@ -354,24 +431,28 @@
354 431 *
355 432 * @return void
356 433 */
357 434 public function select2_ajax_posts_filter_autocomplete() {
358 -
435 + if ( ! wp_verify_nonce( Fns::get_nonce(), rtsb()->nonceText ) ) {
436 + wp_send_json_error( [] );
437 + }
359 438 $query_per_page = 15;
360 439 $post_type = 'post';
361 440 $source_name = 'post_type';
362 - $paged = $_POST['page'] ?? 1;
363 -
441 + $paged = absint( $_POST['page'] ?? 1 );
364 442 if ( ! empty( $_POST['post_type'] ) ) {
443 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
365 444 $post_type = sanitize_text_field( $_POST['post_type'] );
366 445 }
367 446
368 447 if ( ! empty( $_POST['source_name'] ) ) {
448 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
369 449 $source_name = sanitize_text_field( $_POST['source_name'] );
370 450 }
371 451
452 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
372 453 $search = ! empty( $_POST['search'] ) ? sanitize_text_field( $_POST['search'] ) : '';
373 - $results = $post_list = [];
454 + $results = $post_list = []; // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.Found
374 455 switch ( $source_name ) {
375 456 case 'taxonomy':
376 457 $args = [
377 458 'hide_empty' => false,
@@ -376,13 +457,13 @@
376 457 $args = [
377 458 'hide_empty' => false,
378 459 'orderby' => 'name',
379 460 'order' => 'ASC',
380 - 'search' => $search,
461 + 'name__like' => $search,
381 462 'number' => '5',
382 463 ];
383 464
384 - if ( $post_type !== 'all' ) {
465 + if ( 'all' !== $post_type ) {
385 466 $args['taxonomy'] = $post_type;
386 467 }
387 468
388 469 $post_list = wp_list_pluck( get_terms( $args ), 'name', 'term_id' );
@@ -429,17 +510,22 @@
429 510 * @return void
430 511 */
431 512 public function select2_ajax_get_posts_value_titles() {
432 513
514 + if ( ! wp_verify_nonce( Fns::get_nonce(), rtsb()->nonceText ) ) {
515 + wp_send_json_error( [] );
516 + }
517 +
433 518 if ( empty( $_POST['id'] ) ) {
434 519 wp_send_json_error( [] );
435 520 }
436 521
522 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
437 523 if ( empty( array_filter( $_POST['id'] ) ) ) {
438 524 wp_send_json_error( [] );
439 525 }
440 526 $ids = array_map( 'intval', $_POST['id'] );
441 - $source_name = ! empty( $_POST['source_name'] ) ? sanitize_text_field( $_POST['source_name'] ) : '';
527 + $source_name = ! empty( $_POST['source_name'] ) ? sanitize_text_field( $_POST['source_name'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
442 528
443 529 switch ( $source_name ) {
444 530 case 'taxonomy':
445 531 $args = [
@@ -448,9 +534,11 @@
448 534 'order' => 'ASC',
449 535 'include' => implode( ',', $ids ),
450 536 ];
451 537
452 - if ( $_POST['post_type'] !== 'all' ) {
538 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
539 + if ( 'all' !== $_POST['post_type'] ) {
540 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
453 541 $args['taxonomy'] = sanitize_text_field( $_POST['post_type'] );
454 542 }
455 543
456 544 $response = wp_list_pluck( get_terms( $args ), 'name', 'term_id' );
@@ -468,9 +556,9 @@
468 556 break;
469 557 default:
470 558 $post_info = get_posts(
471 559 [
472 - 'post_type' => sanitize_text_field( $_POST['post_type'] ),
560 + 'post_type' => sanitize_text_field( $_POST['post_type'] ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotValidated
473 561 'include' => implode( ',', $ids ),
474 562 ]
475 563 );
476 564 $response = wp_list_pluck( $post_info, 'post_title', 'ID' );
@@ -486,12 +574,12 @@
486 574
487 575 /**
488 576 * Ajax callback for rt-select2
489 577 *
490 - * @param $post_type
491 - * @param $limit
492 - * @param $search
493 - * @param $paged
578 + * @param string $post_type Post type.
579 + * @param int $limit Limit.
580 + * @param string $search Search.
581 + * @param int $paged Paged.
494 582 *
495 583 * @return array
496 584 */
497 585 public function get_query_data( $post_type = 'any', $limit = 10, $search = '', $paged = 1 ) {
@@ -528,10 +616,19 @@
528 616 if ( ! empty( $search ) ) {
529 617 $where .= $wpdb->prepare( " AND {$wpdb->posts}.post_title LIKE %s", '%' . esc_sql( $search ) . '%' );
530 618 }
531 619
620 + // Add a query for shop_coupon post-type to check 'show on frontend' meta.
621 + if ( 'shop_coupon' === $post_type ) {
622 + $where .= " AND {$wpdb->posts}.ID IN (
623 + SELECT post_id FROM {$wpdb->postmeta}
624 + WHERE meta_key = 'rtsb_show_on_frontend'
625 + AND meta_value = 'on'
626 + )";
627 + }
628 +
532 629 $query = "select post_title,ID from $wpdb->posts where post_status = 'publish' {$where} {$limit}";
533 - $results = $wpdb->get_results( $query );
630 + $results = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching
534 631
535 632 if ( ! empty( $results ) ) {
536 633 foreach ( $results as $row ) {
537 634 $data[ $row->ID ] = $row->post_title . ' [#' . $row->ID . ']';
@@ -562,10 +659,10 @@
562 659
563 660 $pro_widgets = [
564 661 [
565 662 'name' => 'rtsb-ajax-product-filters',
566 - 'title' => esc_html__( 'Ajax Product Filters', 'testimonial-slider-showcase' ),
567 - 'description' => esc_html__( 'Ajax Product Filters', 'testimonial-slider-showcase' ),
663 + 'title' => esc_html__( 'Ajax Product Filters', 'shopbuilder' ),
664 + 'description' => esc_html__( 'Ajax Product Filters', 'shopbuilder' ),
568 665 'icon' => 'rtsb-el-custom rtsb-element icon-rtsb-ajax-product-filters rtsb-promotional-element',
569 666 'categories' => '[ "rtsb-shopbuilder" ]',
570 667 ],
571 668 ];