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/Elementor/Render/Render.php +959 -410 2.1.2 → 3.4.2 View file →
@@ -1,9 +1,9 @@
1 1 <?php
2 2 /**
3 3 * Elementor Render Class.
4 4 *
5 - * This class contains render logics & output.
5 + * This class contains render logics & output. It utilizes WC_Products_Query().
6 6 *
7 7 * @package RadiusTheme\SB
8 8 */
9 9
@@ -8,10 +8,14 @@
8 8 */
9 9
10 10 namespace RadiusTheme\SB\Elementor\Render;
11 11
12 +use RadiusTheme\SBPRO\Modules\CatalogMode\CatalogModeFns;
13 +use WC_Query;
12 14 use WP_Query;
13 15 use WC_Product;
16 +use Elementor\Utils;
17 +use WC_Product_Query;
14 18 use RadiusTheme\SB\Helpers\Fns;
15 19 use RadiusTheme\SB\Models\QueryArgs;
16 20 use RadiusTheme\SB\Helpers\BuilderFns;
17 21 use RadiusTheme\SB\Traits\SingletonTrait;
@@ -51,8 +55,15 @@
51 55 */
52 56 public $get_settings;
53 57
54 58 /**
59 + * Element attributes.
60 + *
61 + * @var array
62 + */
63 + private $attributes = [];
64 +
65 + /**
55 66 * Get elementor settings.
56 67 *
57 68 * @return array
58 69 */
@@ -67,140 +78,87 @@
67 78 * @param array $settings Settings.
68 79 * @param string $class Class.
69 80 * @param string $template Template name.
70 81 * @param bool $fullwidth Fullwidth check.
71 - * @param bool $ajax_attr Ajax attributes.
82 + * @param bool $ajax Ajax attributes.
72 83 *
73 84 * @return string
74 85 */
75 - public function container( $content, $settings, $class = '', $template = '', $fullwidth = false, $ajax_attr = true ) {
76 - $layout_id = 'rtsb-container-' . absint( wp_rand() );
77 - $container_attr = null;
78 - $cart_txt_class = '';
79 - $container_class = 'rtsb-elementor-container products rtsb-pos-r ' . esc_attr( $class );
80 - $metas = RenderHelpers::meta_dataset( $settings, $template );
86 + public function container( $content, $settings, $class = '', $template = '', $fullwidth = false, $ajax = true ) {
87 + $rand = wp_rand();
88 + $layout_id = 'rtsb-container-' . $rand;
89 + $metas = RenderHelpers::meta_dataset( $settings, $template );
90 + $raw_layout = esc_html( $metas['layout'] );
91 + $layout = RenderHelpers::filter_layout( $raw_layout, $template );
92 + $style_attr = '--rtsb-default-columns: ' . esc_attr( RenderHelpers::default_columns( $layout ) );
93 + $view_mode = $settings['view_mode'] ?? 'grid';
94 + $view_mode = isset( $_GET['displayview'] ) ? sanitize_text_field( wp_unslash( $_GET['displayview'] ) ) : $view_mode; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
95 + $tooltip_attr = 'list' == $view_mode && ! empty( $metas['tooltip_position_list'] ) ? esc_attr( $metas['tooltip_position_list'] ) : $metas['tooltip_position'];
96 + $has_pro = rtsb()->has_pro();
97 + $has_filters = $has_pro && ! empty( $settings['tax_filter'] );
98 + $grid_style = $settings['grid_style'] ?? 'even';
81 99
82 - $layout = $metas['layout'];
83 - $animation = $metas['hover_animation'];
84 - $animation .= ' gallery-hover-' . ( ! empty( $settings['gallery_hover_animation'] ) ? $settings['gallery_hover_animation'] : 'fade' );
85 -
86 - $is_cat = preg_match( '/category/', $layout );
87 - $is_carousel = preg_match( '/slider/', $layout );
88 -
89 - if ( ! $is_cat ) {
90 - $cart_txt_class = $metas['show_cart_text'] ? ' has-cart-text' : ' no-cart-text';
91 - }
92 -
93 - if ( $is_carousel ) {
94 - $fullwidth = true;
95 - }
96 -
97 - $badge_class = [
98 - 'position' => $metas['badge_position'],
99 - 'alignment' => $metas['badge_alignment'],
100 - ];
101 -
102 - if ( ! rtsb()->has_pro() && ! in_array( $layout, array_keys( Fns::free_layouts() ), true ) ) {
100 + // Set default layout.
101 + if ( ! $has_pro && ! in_array( $layout, array_keys( Fns::free_layouts() ), true ) ) {
103 102 $layout = RenderHelpers::set_default_layout( $layout );
104 103 }
105 104
106 - $has_tab_filters = rtsb()->has_pro() && ! empty( $settings['tax_filter'] );
107 -
108 - if ( $is_carousel ) {
109 - $pagination_attr = $has_tab_filters ? RenderHelpers::pagination_data( $settings, $template ) : '';
110 - } elseif ( $is_cat ) {
111 - $pagination_attr = '';
112 - } else {
113 - $ajax_pagination = ! empty( $settings['show_pagination'] ) && 'pagination' !== $settings['pagination_type'];
114 - $page = Fns::product_filters_has_ajax( 'shop' ) || Fns::product_filters_has_ajax( 'archive' );
115 - $condition = rtsb()->has_pro() && $ajax_attr && ( $ajax_pagination || $has_tab_filters || $page );
116 - $pagination_attr = $condition ? RenderHelpers::pagination_data( $settings, $template ) : '';
105 + if ( $has_pro && 'masonry' === $grid_style ) {
106 + $style_attr .= '; --rtsb-masonry-columns: ' . esc_attr( RenderHelpers::default_columns( $layout ) );
107 + $style_attr .= '; --rtsb-masonry-list-columns: ' . esc_attr( RenderHelpers::default_columns( $layout ) );
117 108 }
118 109
119 - if ( ! in_array( 'clear-btn', $metas['visibility'], true ) ) {
120 - $container_class .= ' no-clear-btn';
121 - }
110 + // Container classes.
111 + $container_classes = RenderHelpers::prepare_container_classes( $metas, $settings, $class );
122 112
123 - $container_class .= ! empty( $metas['pagination'] ) ? ' rtsb-has-pagination' : ' rtsb-no-pagination';
124 - $style_attr = ' style="--rtsb-default-columns: ' . esc_attr( RenderHelpers::default_columns( $layout ) ) . '"';
125 - $tooltip_attr = ' data-tooltip-position="' . esc_attr( $metas['tooltip_position'] ) . '"';
113 + // Pagination attributes.
114 + $pagination_attributes = RenderHelpers::prepare_pagination_attr( $layout, $has_filters, $template, $settings, $ajax );
126 115
127 - $view_mode = $settings['view_mode'] ?? 'grid';
116 + // Attributes for the container.
117 + $container_attributes = [
118 + 'id' => $layout_id,
119 + 'class' => $container_classes,
120 + 'style' => apply_filters( 'rtsb/product/container/attr', $style_attr, $settings ),
121 + 'data-layout' => $layout,
122 + 'data-tooltip-position' => $tooltip_attr,
123 + ];
128 124
129 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended
130 - if ( isset( $_GET['displayview'] ) ) {
131 - $view_mode = sanitize_text_field( wp_unslash( $_GET['displayview'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
125 + if ( ! empty( $pagination_attributes ) ) {
126 + $container_attributes['data-rtsb-ajax'] = $pagination_attributes;
132 127 }
133 128
134 - if ( 'list' == $view_mode && ! empty( $metas['tooltip_position_list'] ) ) {
135 - $tooltip_attr = ' data-tooltip-position="' . esc_attr( $metas['tooltip_position_list'] ) . '"';
129 + // Opt-out of the scroll-to-top behaviour on Ajax filter/pagination when
130 + // the "Enable Scroll To Top?" switch is turned off.
131 + if ( isset( $settings['enable_ajax_scroll'] ) && empty( $settings['enable_ajax_scroll'] ) ) {
132 + $container_attributes['data-rtsb-disable-scroll'] = '1';
136 133 }
137 134
138 - $container_attr .= ' data-layout="' . esc_attr( $layout ) . '"' . $style_attr . $tooltip_attr . $pagination_attr;
139 - $container_class .= ' 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'] );
135 + // Adding render attributes.
136 + $this->add_attribute( 'rtsb_container_attr_' . $rand, $container_attributes );
140 137
141 - if ( $metas['show_overlay'] ) {
142 - $container_class .= ' has-overlay';
143 - }
138 + // Start rendering.
139 + $this->html = '<div ' . $this->get_attribute_string( 'rtsb_container_attr_' . $rand ) . '>';
144 140
145 - if ( in_array( 'single-cat', $metas['visibility'], true ) ) {
146 - $container_class .= ' show-single-cat';
147 - }
141 + // Section title.
142 + $this->html .= $this->render_section_title( $settings );
148 143
149 - if ( ! empty( $settings['show_compare_text'] ) ) {
150 - $container_class .= ' has-compare-text';
151 - }
152 -
153 - if ( ! empty( $settings['show_quick_view_text'] ) ) {
154 - $container_class .= ' has-quick-view-text';
155 - }
156 -
157 - if ( ! empty( $settings['show_wishlist_text'] ) ) {
158 - $container_class .= ' has-wishlist-text';
159 - }
160 -
161 - if ( empty( $settings['show_compare_icon'] ) ) {
162 - $container_class .= ' no-compare-icon';
163 - }
164 -
165 - if ( empty( $settings['show_quick_view_icon'] ) ) {
166 - $container_class .= ' no-quick-view-icon';
167 - }
168 -
169 - if ( empty( $settings['show_wishlist_icon'] ) ) {
170 - $container_class .= ' no-wishlist-icon';
171 - }
172 -
173 - if ( empty( $settings['show_quick_checkout_icon'] ) ) {
174 - $container_class .= ' no-quick_checkout-icon';
175 - }
176 - if ( $is_cat && empty( $settings['show_overlay'] ) ) {
177 - $container_class .= ' no-category-bg-overlay';
178 - }
179 -
180 - $container_class = apply_filters( 'rtsb/product/container/class', $container_class, $settings );
181 - $this->html = '<div class="' . esc_attr( $container_class ) . '" id="' . esc_attr( $layout_id ) . '" ' . $container_attr . '>';
182 -
183 - if ( ! empty( $settings['query_products'] ) && count( $settings['query_products'] ) && 'yes' === $settings['show_section_title'] ) {
184 - $this->html .= '<div class="rtsb-section-title-wrapper">';
185 - $this->html .= '<' . esc_attr( $settings['section_title_tag'] ) . ' class="rtsb-section-title" >';
186 - $this->html .= esc_html( $settings['section_title_text'] );
187 - $this->html .= '</' . esc_attr( $settings['section_title_tag'] ) . '>';
188 - $this->html .= '</div>';
189 - }
190 -
144 + // Content.
191 145 $this->html .= $content;
192 146
147 + // End rendering.
193 148 $this->html .= '</div><!-- .rtsb-elementor-container -->';
194 149
195 150 // Action button icon reset.
196 151 $this->action_button_icon_set_default();
197 152
153 + // Script generator.
198 154 $script_generator = [];
199 155 $script_generator['layout'] = $layout_id;
200 - $script_generator['rand'] = absint( wp_rand() );
201 - $script_generator['isCarousel'] = $is_carousel;
156 + $script_generator['rand'] = absint( $rand );
157 + $script_generator['isCarousel'] = preg_match( '/slider/', $layout );
158 + $script_generator['isMasonry'] = $has_pro && 'masonry' === $grid_style;
202 159
160 + // Register scripts in the wp_footer action.
203 161 add_action(
204 162 'wp_footer',
205 163 static function () use ( $script_generator ) {
206 164 RenderHelpers::register_scripts( $script_generator );
@@ -220,39 +178,42 @@
220 178 *
221 179 * @return string
222 180 */
223 181 public function row( $content, $settings, $hook_html = '', $raw_settings = [] ) {
224 - $masonry_class = null;
225 - $loader_class = ' rtsb-pre-loader';
182 + $raw_layout = esc_html( $settings['layout'] );
183 + $layout = RenderHelpers::filter_layout( $raw_layout );
226 184
227 - $layout = $settings['layout'];
228 - $grid_type = $settings['grid_type'];
229 - $is_carousel = preg_match( '/slider/', $layout );
185 + $rand = wp_rand();
186 + $is_carousel = preg_match( '/slider/', $raw_layout );
230 187
231 - if ( 'even' === $grid_type ) {
232 - $masonry_class = ' rtsb-even';
233 - } elseif ( 'masonry' === $grid_type ) {
234 - $masonry_class = ' rtsb-masonry';
188 + if ( preg_match( '/category/', $raw_layout ) && ( ! empty( $settings['active_cat_slider'] ) ) ) {
189 + $is_carousel = true;
235 190 }
236 191
237 - if ( ! rtsb()->has_pro() || $is_carousel ) {
238 - $masonry_class = ' rtsb-even';
239 - }
192 + // Row classes.
193 + $row_classes = RenderHelpers::prepare_row_classes( $settings );
240 194
241 - $row_start_class = apply_filters( 'rtsb/custom/layout/row/classes', esc_attr( 'rtsb-row rtsb-content-loader element-loading rtsb-' . $layout . $masonry_class . $loader_class ), $settings );
242 - $row_wrapper = '<div data-title="' . esc_html__( 'Loading ...', 'shopbuilder' ) . '" class="' . $row_start_class . '">';
243 - $start_wrapper = apply_filters( 'rtsb/custom/layout/row/start', $row_wrapper, $row_start_class );
244 - if ( ! empty( $settings['show_filter'] ) ) {
245 - $this->html = $hook_html;
246 - $this->html .= $start_wrapper;
247 - } else {
248 - $this->html = $start_wrapper;
249 - }
195 + // Adding render attributes.
196 + $this->add_attribute( 'rtsb_row_attr_' . $rand, 'class', $row_classes );
250 197
198 + // Start rendering.
199 + $row_wrapper = '<div ' . $this->get_attribute_string( 'rtsb_row_attr_' . $rand ) . '>';
200 + $start_wrapper = apply_filters( 'rtsb/elementor/row/start', $row_wrapper, $raw_settings );
201 +
202 + $this->html = ! empty( $settings['show_filter'] ) ? $hook_html . $start_wrapper : $start_wrapper;
203 +
251 204 if ( $is_carousel ) {
252 205 $slider_data = RenderHelpers::slider_data( $settings, $raw_settings );
206 + // Adding slider render attributes.
207 + $this->add_attribute(
208 + 'rtsb_slider_attr_' . $rand,
209 + [
210 + 'class' => 'rtsb-col-grid ' . $slider_data['class'],
211 + 'data-options' => $slider_data['data'],
212 + ]
213 + );
253 214
254 - $this->html .= '<div class="rtsb-col-grid ' . esc_attr( $slider_data['class'] ) . '" ' . $slider_data['data'] . '>';
215 + $this->html .= '<div ' . $this->get_attribute_string( 'rtsb_slider_attr_' . $rand ) . '>';
255 216 $this->html .= '<div class="swiper-wrapper">';
256 217 }
257 218
258 219 $this->html .= $content;
@@ -259,32 +220,18 @@
259 220
260 221 if ( $is_carousel ) {
261 222 $this->html .= '</div><!-- .swiper-wrapper -->';
262 223
263 - $left_icon = $settings['left_arrow_icon'];
264 - $right_icon = $settings['right_arrow_icon'];
265 -
266 - if ( ! empty( $settings['slider_nav'] ) ) {
267 - $this->html .=
268 - '<div class="swiper-nav">
269 - <div class="swiper-arrow swiper-button-next">
270 - ' . Fns::icons_manager( $right_icon ) . '
271 - </div>
272 - <div class="swiper-arrow swiper-button-prev">
273 - ' . Fns::icons_manager( $left_icon ) . '
274 - </div>
275 - </div>';
276 - }
277 -
278 - $this->html .= ! empty( $settings['slider_dot'] ) ? '<div class="swiper-pagination rtsb-pos-s"></div>' : '';
279 -
224 + // Slider buttons.
225 + $this->html .= $this->render_slider_buttons( $settings );
280 226 $this->html .= '</div><!-- .rtsb-carousel-slider -->';
281 227 }
282 228
283 229 $row_end = '</div><!-- .rtsb-row -->';
284 230
285 - $this->html .= apply_filters( 'rtsb/custom/layout/row/end', $row_end );
231 + $this->html .= apply_filters( 'rtsb/elementor/row/end', $row_end );
286 232
233 + // Preloader.
287 234 $this->html .= RenderHelpers::pre_loader( $layout );
288 235
289 236 return $this->html;
290 237 }
@@ -291,8 +238,63 @@
291 238
292 239 /**
293 240 * Product loop.
294 241 *
242 + * @param object $products Products.
243 + * @param array $settings Settings.
244 + * @param string $template Template.
245 + *
246 + * @return string|null
247 + */
248 + public function product_loop( $products, $settings, $template ) {
249 + $html = null;
250 +
251 + if ( empty( $products ) ) {
252 + return null;
253 + }
254 +
255 + $i = 0;
256 +
257 + global $product;
258 +
259 + $isGlobalProduct = false;
260 +
261 + if ( $product instanceof WC_Product ) {
262 + $isGlobalProduct = $product;
263 + }
264 +
265 + $layout = RenderHelpers::filter_layout( esc_html( $settings['layout'] ), $template );
266 +
267 + foreach ( $products as $_product ) {
268 + $i++;
269 + $GLOBALS['product'] = $_product; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- Global variable is intentionally used to share the current product context with hooks and templates during rendering.
270 + /**
271 + * Before product template render hook.
272 + */
273 + do_action( 'rtsb_before_product_template_render' );
274 +
275 + // Loop arg.
276 + $arg = $this->arg_dataset( $settings, $_product, $settings['lazy_load'], $i );
277 +
278 + // Get template.
279 + $html .= Fns::load_template( $template . $layout, $arg, true );
280 +
281 + /**
282 + * After product template render hook.
283 + */
284 + do_action( 'rtsb_after_product_template_render' );
285 +
286 + unset( $GLOBALS['product'] );
287 + }
288 + if ( $isGlobalProduct ) {
289 + $GLOBALS['product'] = $isGlobalProduct; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- Global variable is intentionally used to share the current product context with hooks and templates during rendering.
290 + }
291 + return $html;
292 + }
293 +
294 + /**
295 + * WC Product loop.
296 + *
295 297 * @param object $query The query.
296 298 * @param array $settings Settings.
297 299 * @param string $template Template.
298 300 *
@@ -297,9 +299,9 @@
297 299 * @param string $template Template.
298 300 *
299 301 * @return string|null
300 302 */
301 - public function product_loop( $query, $settings, $template ) {
303 + public function wc_product_loop( $query, $settings, $template ) {
302 304 $html = null;
303 305
304 306 if ( empty( $query ) ) {
305 307 return null;
@@ -304,19 +306,20 @@
304 306 if ( empty( $query ) ) {
305 307 return null;
306 308 }
307 309
308 - $i = 0;
310 + $i = 0;
311 + $layout = RenderHelpers::filter_layout( esc_html( $settings['layout'] ), $template );
309 312
310 313 while ( $query->have_posts() ) {
311 314 $query->the_post();
312 315 $i++;
313 -
316 + global $product;
314 317 // Loop arg.
315 - $arg = $this->arg_dataset( $settings, get_the_ID(), $settings['lazy_load'], $i );
318 + $arg = $this->arg_dataset( $settings, $product, $settings['lazy_load'], $i );
316 319
317 320 // Get template.
318 - $html .= Fns::load_template( $template . $settings['layout'], $arg, true );
321 + $html .= Fns::load_template( $template . $layout, $arg, true );
319 322 }
320 323
321 324 return $html;
322 325 }
@@ -321,9 +324,9 @@
321 324 return $html;
322 325 }
323 326
324 327 /**
325 - * Product loop.
328 + * Category loop.
326 329 *
327 330 * @param object $query The query.
328 331 * @param array $settings Settings.
329 332 * @param string $template Template.
@@ -333,11 +336,13 @@
333 336 */
334 337 public function category_loop( $query, $settings, $template, $is_single = false ) {
335 338 $html = null;
336 339 $category_loop = false;
340 +
337 341 if ( 'category-layout3' === $settings['raw_settings']['layout'] || 'category-single-layout2' === $settings['raw_settings']['layout'] ) {
338 342 $category_loop = true;
339 343 }
344 +
340 345 if ( $is_single ) {
341 346 // Loop arg.
342 347 $arg = $this->cat_arg_dataset( $settings, $query, true, false, $category_loop );
343 348
@@ -374,42 +379,56 @@
374 379
375 380 // Query Args.
376 381 $args = ( new QueryArgs() )->buildArgs( $metas, 'product', $is_carousel );
377 382
378 - $temp_args = $args;
379 - $args = apply_filters( 'rtsb/elementor/args_before_loop', $args, $metas );
383 + $temp_args = $args;
384 + $default_args = [
385 + 'visibility' => 'visible',
386 + 'status' => 'publish',
387 + ];
388 + $custom_args = apply_filters( 'rtsb/elementor/args_before_loop', $args, $metas );
389 + $args = wp_parse_args( $custom_args, $default_args );
380 390
391 + /**
392 + * Before Product query hook.
393 + *
394 + * @hooked RadiusTheme\SB\Controllers\Hooks\ActionHooks::modify_wc_query_args 10
395 + */
396 +
397 + do_action( 'rtsb/elements/render/before_query', $settings, $args );
381 398 // Query.
382 - $product_query = new WP_Query( $args );
399 + $product_query = new WC_Product_Query( $args );
383 400
384 - if ( $product_query->have_posts() ) {
385 - // Elementor Render hook before row.
386 - ob_start();
387 - do_action( 'rtsb/elementor/render/before_row', $metas, $settings, $product_query, $temp_args );
388 - $hook_html .= ob_get_clean();
401 + /**
402 + * Product query hook.
403 + */
404 + do_action( 'rtsb/elements/render/product_query', $product_query, $this );
389 405
406 + $products = $pagination ? $product_query->get_products()->products : $product_query->get_products();
407 +
408 + if ( ! empty( $products ) ) {
409 + // Start rendering.
410 + $hook_html .= $this->render_start( $metas, $settings, $temp_args );
411 +
390 412 // Row.
391 - $this->row( $this->product_loop( $product_query, $metas, $template ), $metas, $hook_html, $settings );
413 + $this->row( $this->product_loop( $products, $metas, $template ), $metas, $hook_html, $settings );
392 414
393 415 // Pagination.
394 416 if ( $pagination ) {
395 - $this->html .= RenderHelpers::render_pagination(
396 - $product_query,
397 - $metas,
398 - $settings['posts_limit'],
399 - $settings['pagination_per_page']
400 - );
417 + $pagination_args = [
418 + 'query' => $product_query,
419 + 'meta' => $metas,
420 + 'limit' => $settings['posts_limit'],
421 + 'per_page' => $settings['pagination_per_page'],
422 + ];
423 +
424 + $this->html .= $this->render_products_pagination( $pagination_args );
401 425 }
402 426
403 - // Elementor Render hook before row.
404 - ob_start();
405 - do_action( 'rtsb/elementor/render/after_row', $metas, $settings, $product_query );
406 - $this->html .= ob_get_clean();
407 -
427 + // End rendering.
428 + $this->html .= $this->render_end( $metas, $settings, $products );
408 429 } else {
409 - $content = '<div class="woocommerce-no-products-found"><div class="woocommerce-info">' . esc_html__( 'No products were found matching your selection.', 'shopbuilder' ) . '</div></div>';
410 -
411 - $this->row( $content, $metas );
430 + $this->no_products_msg( $metas );
412 431 }
413 432
414 433 wp_reset_postdata();
415 434
@@ -414,8 +433,15 @@
414 433 wp_reset_postdata();
415 434
416 435 unset( $args['tax_query'] );
417 436
437 + /**
438 + * After Product query hook.
439 + *
440 + * @hooked RadiusTheme\SB\Controllers\Hooks\ActionHooks::reset_wc_query_args 10
441 + */
442 + do_action( 'rtsb/elements/render/after_query', $settings, $args );
443 +
418 444 // Container.
419 445 $this->container( $this->html, $settings, 'rtsb-products-container', $template, $is_carousel );
420 446
421 447 return $this->html;
@@ -420,9 +446,8 @@
420 446
421 447 return $this->html;
422 448 }
423 449
424 -
425 450 /**
426 451 * Render Product View.
427 452 *
428 453 * @param string $template Template name.
@@ -434,24 +459,29 @@
434 459 public function wc_loop_for_product_view( $template, $settings, $widget ) {
435 460 global $wp_query, $post;
436 461
437 462 $this->get_settings = $settings;
463 + $rand = wp_rand();
438 464 $metas = RenderHelpers::meta_dataset( $settings, $template, $widget->get_settings_for_display() );
439 465
440 - $settings['rtsb_order'] = ! empty( $wp_query->query_vars['order'] ) ? $wp_query->query_vars['order'] : 'ASC';
441 - $settings['rtsb_orderby'] = ! empty( $wp_query->query_vars['orderby'] ) ? $wp_query->query_vars['orderby'] : 'menu_order';
466 + $order = strtoupper( $wp_query->query_vars['order'] ?? 'ASC' );
467 + $settings['rtsb_orderby'] = RenderHelpers::escaping_product_orderby( $wp_query->query_vars['orderby'] ?? 'menu_order' );
468 + $settings['rtsb_order'] = in_array( $order, [ 'ASC','DESC' ], true ) ? $order : 'ASC';
442 469
443 470 $pagination = $metas['pagination'];
444 471
445 - $page_type = BuilderFns::builder_type( get_the_ID() );
446 - $is_preview = BuilderFns::is_builder_preview() && array_key_exists( $page_type, BuilderFns::builder_page_types() );
447 - $posts_per_page = wc_get_default_products_per_row() * wc_get_default_product_rows_per_page();
448 -
472 + $page_type = BuilderFns::builder_type( get_the_ID() );
473 + $is_preview = BuilderFns::is_builder_preview() && array_key_exists( $page_type, BuilderFns::builder_page_types() );
474 + $raw_settings = $widget->get_settings_for_display();
475 + $widget_per_page = ! empty( $raw_settings['posts_per_page'] ) ? absint( $raw_settings['posts_per_page'] ) : 0;
476 + $wc_per_page = wc_get_default_products_per_row() * wc_get_default_product_rows_per_page();
477 + $posts_per_page = $widget_per_page ? $widget_per_page : apply_filters( 'loop_shop_per_page', $wc_per_page ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- woocommerce default hooks used.
478 + $restore_query = false;
449 479 if ( $is_preview ) {
450 480 $main_query = clone $wp_query;
451 481 $main_post = clone $post;
452 482
453 - $ordering = ( new \WC_Query() )->get_catalog_ordering_args();
483 + $ordering = ( new WC_Query() )->get_catalog_ordering_args();
454 484 $wp_query_args = [
455 485 'post_type' => 'product',
456 486 'posts_per_page' => $posts_per_page,
457 487 ];
@@ -464,61 +494,111 @@
464 494 $wp_query_args['order'] = $ordering['order'];
465 495 }
466 496
467 497 $wp_query = new WP_Query( $wp_query_args ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
498 +
468 499 wc_set_loop_prop( 'total', $wp_query->found_posts );
469 500 wc_set_loop_prop( 'total_pages', $wp_query->max_num_pages );
501 + } elseif ( ! is_admin() && ( is_shop() || is_product_taxonomy() ) && absint( $wp_query->get( 'posts_per_page' ) ) !== $posts_per_page ) {
502 + // Apply widget's posts_per_page to the main query on the live frontend.
503 + $main_query = clone $wp_query;
504 + $main_post = clone $post;
505 +
506 + $query_vars = $wp_query->query_vars;
507 + $query_vars['posts_per_page'] = $posts_per_page;
508 +
509 + $wp_query = new WP_Query( $query_vars ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
510 +
511 + wc_set_loop_prop( 'total', $wp_query->found_posts );
512 + wc_set_loop_prop( 'total_pages', $wp_query->max_num_pages );
513 +
514 + $restore_query = true;
470 515 }
471 516
517 + if ( rtsb()->has_pro() && ( Fns::product_has_applied_filters( 'shop' ) || Fns::product_has_applied_filters( 'archive' ) || 'rtsb_builder' === get_post_type( get_the_ID() ) ) ) {
518 + $rtsb_active_filters_html = Fns::get_active_filters_html();
519 + $rtsb_wrapper_style = '' !== $rtsb_active_filters_html ? ' style="' . esc_attr( 'display:block;' ) . '"' : '';
520 + echo '<div class="rtsb-active-filters-wrapper"' . $rtsb_wrapper_style . '>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Style value is escaped above.
521 + Fns::print_html( $rtsb_active_filters_html, true );
522 + echo '</div>';
523 + }
524 +
472 525 // column_per_row.
473 526 if ( woocommerce_product_loop() ) {
474 527 if ( wc_get_loop_prop( 'total' ) ) {
475 - if ( Fns::product_has_applied_filters( 'shop' ) || Fns::product_has_applied_filters( 'archive' ) || 'rtsb_builder' === get_post_type( get_the_ID() ) ) {
476 - echo '<div class="rtsb-active-filters-wrapper"></div>';
477 - }
478 528
479 529 // Row.
480 - $this->row( $this->product_loop( $wp_query, $metas, $template ), $metas );
530 + $this->row( $this->wc_product_loop( $wp_query, $metas, $template ), $metas, '', $settings );
481 531 }
482 532
483 533 // Pagination.
484 - $this->html .= '<div class="rtsb-archive-pagination-wrap">';
534 + $this->html .= '<div class="rtsb-archive-pagination-wrap" data-per-page="' . absint( $posts_per_page ) . '">';
485 535
486 536 if ( $pagination ) {
487 - if ( Fns::product_filters_has_ajax( 'shop' ) || Fns::product_filters_has_ajax( 'archive' ) ) {
537 + if ( Fns::product_filters_has_ajax( apply_filters( 'rtsb/builder/set/current/page/type', '' ) ) ) {
488 538 if ( $wp_query->max_num_pages > 1 ) {
489 - $this->html .= "
490 - <div class='rtsb-load-more rtsb-pos-r'>
491 - <button data-paged='2' data-max-page='{$wp_query->max_num_pages}'><span>" . esc_html__( 'Load More', 'shopbuilder' ) . "</span></button>
492 - <div class='rtsb-loadmore-loading rtsb-ball-clip-rotate'><div></div></div>
493 - </div>
494 - ";
539 + $posts_loading_type = ! empty( $metas['posts_loading_type'] ) ? $metas['posts_loading_type'] : 'pagination';
540 +
541 + if ( 'pagination_ajax' === $posts_loading_type ) {
542 + $this->html .= Fns::custom_pagination( $wp_query->max_num_pages, $posts_per_page, true, $metas['visible_range'] ?? null );
543 + } elseif ( 'pagination' === $posts_loading_type ) {
544 + $this->html .= Fns::custom_pagination( $wp_query->max_num_pages, $posts_per_page, false, $metas['visible_range'] ?? null );
545 + } elseif ( 'load_on_scroll' === $posts_loading_type ) {
546 + $this->html .= "<div class='rtsb-infinite-scroll' data-trigger='1' data-paged='2' data-max-page='{$wp_query->max_num_pages}'>
547 + <div class='rtsb-infinite-action'>
548 + <div class='rtsb-loadmore-loading rtsb-ball-clip-rotate'><div></div></div>
549 + </div>
550 + </div>";
551 + } else {
552 + $load_more_text = ! empty( $metas['load_more_text'] ) ? $metas['load_more_text'] : esc_html__( 'Load More', 'shopbuilder' );
553 +
554 + $this->add_attribute(
555 + 'rtsb_load_more_btn_attr_' . $rand,
556 + [
557 + 'data-paged' => '2',
558 + 'data-max-page' => $wp_query->max_num_pages,
559 + ]
560 + );
561 +
562 + $this->html .=
563 + "<div class='rtsb-load-more rtsb-pos-r'>
564 + <button {$this->get_attribute_string( 'rtsb_load_more_btn_attr_' . $rand )}>
565 + <span>" . esc_html( $load_more_text ) . "</span>
566 + </button>
567 + <div class='rtsb-loadmore-loading rtsb-ball-clip-rotate'><div></div></div>
568 + </div>";
569 + }
495 570 }
496 571 } else {
497 - $this->html .= RenderHelpers::render_pagination(
498 - $wp_query,
499 - $metas,
500 - wc_get_loop_prop( 'total' ),
501 - $posts_per_page
502 - );
572 + $pagination_args = [
573 + 'query' => $wp_query,
574 + 'meta' => $metas,
575 + 'limit' => wc_get_loop_prop( 'total' ),
576 + 'per_page' => $posts_per_page,
577 + ];
578 +
579 + $this->html .= $this->render_products_pagination( $pagination_args, 'wp' );
503 580 }
504 581 }
505 582
506 583 $this->html .= '</div>';
507 584 } else {
508 - $content = '<div class="woocommerce-no-products-found"><div class="woocommerce-info">' . esc_html__( 'No products were found matching your selection.', 'shopbuilder' ) . '</div></div>';
509 -
510 - $this->row( $content, $metas );
585 + $this->no_products_msg( $metas );
511 586 }
512 587
513 - if ( $is_preview ) {
588 + if ( $is_preview || ! empty( $restore_query ) ) {
589 + // phpcs:disable
514 590 $wp_query = $main_query;
515 591 $post = $main_post;
516 592
517 593 wp_reset_query();
518 594 wp_reset_postdata();
595 + // phpcs:enable
519 596 }
520 597
598 + // Ensure the container's data-rtsb-ajax uses the same posts_per_page as the initial render.
599 + $settings['pagination_per_page'] = $posts_per_page;
600 +
521 601 // Container.
522 602 $this->container( $this->html, $settings, 'rtsb-products-container', $template );
523 603
524 604 return $this->html;
@@ -524,12 +604,13 @@
524 604 return $this->html;
525 605 }
526 606
527 607 /**
528 - * Render Product View.
608 + * Loop Setup for Product View.
529 609 *
530 610 * @param string $template Template name.
531 611 * @param array $settings Control settings.
612 + * @param object $widget Widget object.
532 613 *
533 614 * @return string
534 615 */
535 616 public function setup_loop_for_product_view( $template, $settings, $widget ) {
@@ -546,13 +627,13 @@
546 627
547 628 setup_postdata( $GLOBALS['post'] =& $post_object ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited, Squiz.PHP.DisallowMultipleAssignments.Found
548 629
549 630 // Loop arg.
550 - $arg = $this->arg_dataset( $metas, $_product->get_id() );
631 + $arg = $this->arg_dataset( $metas, $_product );
551 632 // Get template.
552 633 $html .= Fns::load_template( $template . $settings['layout'], $arg, true );
553 634
554 - $this->row( $html, $metas );
635 + $this->row( $html, $metas, '', $settings );
555 636 }
556 637
557 638 // Container.
558 639 $this->container( $this->html, $settings, 'rtsb-products-container', '', false, false );
@@ -571,8 +652,13 @@
571 652 *
572 653 * @return string
573 654 */
574 655 public function category_view( $template, $settings, $is_single = false ) {
656 + // Reset the shared singleton's html buffer so leftover markup from a
657 + // previously rendered widget (e.g. a products grid that ran earlier
658 + // in the same request) doesn't get folded into the categories
659 + // container by container() below.
660 + $this->html = '';
575 661 $this->get_settings = $settings;
576 662
577 663 $metas = RenderHelpers::meta_dataset( $settings, $template, $this->get_settings_for_display() );
578 664 $taxonomy = $metas['category_source'];
@@ -580,8 +666,11 @@
580 666
581 667 if ( rtsb()->has_pro() && ( ! empty( $metas['tag_term'] ) ) ) {
582 668 $term = $metas['tag_term'];
583 669 }
670 + if ( rtsb()->has_pro() && ( ! empty( $metas['brand_term'] ) ) ) {
671 + $term = $metas['brand_term'];
672 + }
584 673
585 674 // Query.
586 675 if ( $is_single ) {
587 676 if ( empty( $term ) ) {
@@ -599,9 +688,9 @@
599 688 if ( ! is_wp_error( $taxonomy_query ) && ! empty( $taxonomy_query ) ) {
600 689 // Row.
601 690 $this->row( $this->category_loop( $taxonomy_query, $metas, $template, $is_single ), $metas );
602 691 } else {
603 - $this->html .= '<p>' . esc_html__( 'No categories found', 'shopbuilder' ) . '</p>';
692 + $this->html = '<p>' . esc_html__( 'No categories found', 'shopbuilder' ) . '</p>';
604 693 }
605 694
606 695 // Container.
607 696 $this->container( $this->html, $settings, 'rtsb-categories-container', $is_single );
@@ -648,8 +737,34 @@
648 737
649 738 /**
650 739 * Builds an array with meta values.
651 740 *
741 + * @param array $meta Meta values.
742 + * @param object $product Product object.
743 + * @param bool $lazy_load Lazy Load.
744 + * @param int $i Integer.
745 + *
746 + * @return array
747 + */
748 + public function arg_dataset( array $meta, object $product, bool $lazy_load = false, $i = 0 ) {
749 + $post_id = $product->get_id();
750 + $arg = $this->build_product_arg(
751 + [
752 + 'product' => $product,
753 + 'id' => $product->get_id(),
754 + 'type' => $product->get_type(),
755 + 'meta' => $meta,
756 + 'lazy_load' => $lazy_load,
757 + 'i' => $i,
758 + ]
759 + );
760 +
761 + return apply_filters( 'rtsb/elementor/render/arg_dataset', $arg, $post_id, $meta, $lazy_load );
762 + }
763 +
764 + /**
765 + * Builds an array with meta values for WP_Query.
766 + *
652 767 * @param array $meta Meta values.
653 768 * @param int $post_id Post ID.
654 769 * @param bool $lazy_load Lazy Load.
655 770 * @param int $i Integer.
@@ -655,22 +770,184 @@
655 770 * @param int $i Integer.
656 771 *
657 772 * @return array
658 773 */
659 - public function arg_dataset( array $meta, int $post_id, bool $lazy_load = false, $i = 0 ) {
774 + public function wp_arg_dataset( array $meta, int $post_id, bool $lazy_load = false, $i = 0 ) {
775 + // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
660 776 if ( ( empty( $meta ) && ! $post_id ) || ! in_array( get_post_type( $post_id ), [ 'product_variation', 'product' ] ) ) {
661 777 return [];
662 778 }
663 779
664 - global $post, $product;
780 + global $product;
665 781
666 782 if ( ! $product instanceof WC_Product && $post_id ) {
667 - $product = wc_get_product( $post_id );
783 + $product = wc_get_product( $post_id ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- Global variable is intentionally used to share the current product context with hooks and templates during rendering.
668 784 }
669 785
670 - $arg = [];
671 - $p_type = $product->get_type();
786 + $post_id = $product->get_id();
787 + $arg = $this->build_product_arg(
788 + [
789 + 'product' => $product,
790 + 'id' => $post_id,
791 + 'type' => $product->get_type(),
792 + 'meta' => $meta,
793 + 'lazy_load' => $lazy_load,
794 + 'i' => $i,
795 + ]
796 + );
672 797
798 + return apply_filters( 'rtsb/elementor/render/arg_dataset', $arg, $post_id, $meta, $lazy_load );
799 + }
800 +
801 + /**
802 + * Builds an array with meta values.
803 + *
804 + * @param array $meta Meta values.
805 + * @param Object $query Category object.
806 + * @param bool $fullwidth Fullwidth check.
807 + * @param bool $lazy_load Lazy Load.
808 + * @param bool $category_loop Category loop.
809 + *
810 + * @return array
811 + */
812 + public function cat_arg_dataset( $meta, $query = null, $fullwidth = false, $lazy_load = false, $category_loop = false ) {
813 + if ( empty( $meta ) ) {
814 + return [];
815 + }
816 +
817 + $arg = [];
818 +
819 + $products = null;
820 + $arg['class'] = null;
821 + $arg['grid'] = null;
822 + $arg['anchor_class'] = null;
823 + $arg['p_sale'] = null;
824 + $arg['count'] = max( $query->count, 0 );
825 + $arg['count_position'] = $meta['count_position'];
826 + $arg['excerpt_position'] = $meta['cat_excerpt_position'];
827 + $arg['cat_link'] = get_term_link( $query );
828 + $arg['target'] = '_self';
829 + $arg['raw_settings'] = ! empty( $meta['raw_settings'] ) ? $meta['raw_settings'] : [];
830 +
831 + if ( ! empty( $meta['tag_term'] ) ) {
832 + $arg['tag_term'] = $meta['tag_term'];
833 + }
834 +
835 + if ( ! empty( $meta['custom_link']['url'] ) ) {
836 + $arg['cat_link'] = $meta['custom_link']['url'];
837 + $arg['target'] = $meta['custom_link']['is_external'] ? '_blank' : '_self';
838 + }
839 +
840 + $title = $query->name;
841 + $excerpt = $query->description;
842 +
843 + if ( in_array( 'badges', $meta['visibility'], true ) ) {
844 + $arg['p_sale'] = $meta['custom_badge_text'];
845 + }
846 +
847 + $arg['badge_class'] = RenderHelpers::badge_class( $meta['badge_preset'] );
848 +
849 + if ( $arg['count'] >= 0 ) {
850 + $prefix = ! empty( $meta['before_count'] ) ? $meta['before_count'] : '';
851 + $suffix = ! empty( $meta['after_count'] ) ? $meta['after_count'] : '';
852 +
853 + $arg['count'] = $prefix . $arg['count'] . $suffix;
854 + }
855 +
856 + if ( $meta['show_custom_title'] && ! empty( $meta['cat_custom_title'] ) ) {
857 + $title = $meta['cat_custom_title'];
858 + }
859 +
860 + if ( $meta['show_custom_excerpt'] && ! empty( $meta['cat_custom_excerpt'] ) ) {
861 + $excerpt = $meta['cat_custom_excerpt'];
862 + }
863 +
864 + $arg['c_img'] = $meta['c_img'];
865 + $arg['items'] = $meta['visibility'];
866 + $arg['title_tag'] = $meta['title_tag'];
867 + $arg['title_class'] = 'category-title rtsb-text-limit limit-' . $meta['title_limit'] . ( ! $meta['title_link'] ? ' no-link' : '' );
868 + $arg['title'] = Fns::text_truncation( $title, $meta['title_limit_custom'] );
869 + $arg['excerpt_limit'] = $meta['excerpt_limit'];
870 + $arg['excerpt'] = wpautop( Fns::text_truncation( $excerpt, $meta['excerpt_limit_custom'] ) );
871 + $arg['title_link'] = $meta['title_link'];
872 + $arg['image_link'] = $meta['image_link'];
873 + $arg['grid'] .= 'rtsb-col-grid ';
874 +
875 + if ( ! $fullwidth ) {
876 + if ( 'masonry' === $meta['grid_type'] ) {
877 + $arg['class'] .= 'masonry-grid-item ';
878 + } else {
879 + $arg['class'] .= 'even-grid-item ';
880 + }
881 + }
882 +
883 + if ( ! empty( $meta['active_cat_slider'] ) ) {
884 + $arg['class'] .= ' rtsb-slide-item swiper-slide animated rtFadeIn ';
885 + }
886 +
887 + $arg['class'] .= 'rtsb-category-grid';
888 +
889 + ob_start();
890 + do_action( 'rtsb/categories/category_image_override', $meta, $query->term_id );
891 + $override_category_image = ob_get_clean();
892 +
893 + $arg['img_html'] = $meta['c_img'] ? Fns::get_product_image_html(
894 + 'category',
895 + $query->term_id,
896 + $meta['f_img_size'],
897 + $meta['default_img_id'],
898 + $meta['custom_img_size'],
899 + $lazy_load,
900 + false,
901 + false,
902 + $override_category_image
903 + ) : null;
904 +
905 + if ( $meta['show_custom_image'] && ! empty( $meta['custom_image'] ) ) {
906 + $arg['img_html'] = Fns::get_product_image_html(
907 + 'custom',
908 + null,
909 + $meta['f_img_size'],
910 + $meta['custom_image'],
911 + $meta['custom_img_size'],
912 + $lazy_load
913 + );
914 + }
915 +
916 + $arg['excerpt_class'] = 'category-info' . ( 'block' === $meta['count_position'] ? ' block-count' : ' inline-count' ) . ( 'above' === $meta['cat_excerpt_position'] ? ' excerpt-above' : '' ) . ( empty( $arg['excerpt'] ) ? ' no-excerpt' : '' );
917 +
918 + if ( $category_loop ) {
919 + $query_args = [
920 + 'product_category_id' => $query->term_id,
921 + 'limit' => -1,
922 + 'order' => 'DESC',
923 + 'orderby' => 'date',
924 + ];
925 +
926 + $products_query = new WC_Product_Query( $query_args );
927 + $products = $products_query->get_products();
928 + }
929 +
930 + return apply_filters( 'rtsb/elementor/render/cat_arg_dataset', $arg, $meta, $query, $fullwidth, $lazy_load, $products );
931 + }
932 +
933 + /**
934 + * Building product args.
935 + *
936 + * @param array $args Args.
937 + * @return array
938 + */
939 + public function build_product_arg( $args ) {
940 + list(
941 + 'product' => $product,
942 + 'id' => $post_id,
943 + 'type' => $p_type,
944 + 'meta' => $meta,
945 + 'lazy_load' => $lazy_load,
946 + 'i' => $i,
947 + ) = $args;
948 +
949 + $arg = [];
673 950 $arg['class'] = null;
674 951 $arg['grid'] = null;
675 952 $arg['anchor_class'] = null;
676 953 $arg['hover_img_html'] = null;
@@ -675,26 +952,28 @@
675 952 $arg['anchor_class'] = null;
676 953 $arg['hover_img_html'] = null;
677 954 $arg['s_link'] = [];
678 955 $arg['add_to_cart'] = '';
956 + $arg['p_counter'] = absint( $i );
679 957 $arg['p_id'] = $post_id;
680 - $arg['p_link'] = get_permalink();
958 + $arg['product'] = $product;
959 + $arg['p_link'] = $product->get_permalink();
681 960 $arg['p_sale'] = '';
682 961 $arg['rating_args'] = [];
683 962 $arg['has_attributes'] = $product->is_type( 'variable' ) && $product->get_attributes();
684 963 $arg['raw_settings'] = ! empty( $meta['raw_settings'] ) ? $meta['raw_settings'] : [];
685 964 $cart_icon_html = '';
965 + $layout = $meta['layout'];
686 966
687 - $layout = $meta['layout'];
688 -
967 + // Building the arg.
689 968 $arg['target'] = '_self';
690 969 $arg['f_img'] = $meta['f_img'];
691 970 $arg['items'] = $meta['visibility'];
692 971 $arg['title_tag'] = $meta['title_tag'];
693 972 $arg['title_class'] = 'product-title rtsb-text-limit limit-' . $meta['title_limit'] . ( ! $meta['title_link'] ? ' no-link' : '' );
694 - $arg['title'] = Fns::text_truncation( get_the_title(), $meta['title_limit_custom'] );
973 + $arg['title'] = Fns::text_truncation( $product->get_title(), $meta['title_limit_custom'] );
695 974 $arg['excerpt_limit'] = $meta['excerpt_limit'];
696 - $arg['excerpt'] = wpautop( Fns::text_truncation( do_shortcode( $post->post_excerpt ), $meta['excerpt_limit_custom'] ) );
975 + $arg['excerpt'] = wpautop( Fns::text_truncation( do_shortcode( get_post_field( 'post_excerpt', $post_id ) ), $meta['excerpt_limit_custom'] ) );
697 976 $arg['title_link'] = $meta['title_link'];
698 977 $arg['image_link'] = $meta['image_link'];
699 978 $arg['out_of_stock'] = $meta['out_of_stock_badge_text'];
700 979 $arg['action_btn_preset'] = $meta['btn_preset'];
@@ -706,10 +985,15 @@
706 985 $is_list = preg_match( '/list/', $layout );
707 986 $is_carousel = preg_match( '/slider/', $layout );
708 987
709 988 if ( ! $is_carousel ) {
710 - $arg['grid'] .= 'rtsb-col-grid ';
711 - $arg['class'] .= ' even-grid-item ';
989 + $arg['grid'] .= 'rtsb-col-grid ';
990 +
991 + if ( 'masonry' === $meta['grid_type'] ) {
992 + $arg['class'] .= 'masonry-grid-item ';
993 + } else {
994 + $arg['class'] .= 'even-grid-item ';
995 + }
712 996 } else {
713 997 $arg['grid'] .= 'rtsb-col-grid rtsb-col-full rtsb-slide-item swiper-slide ';
714 998 }
715 999
@@ -729,17 +1013,21 @@
729 1013
730 1014 $arg['class'] .= ' animated rtFadeIn';
731 1015
732 1016 if ( in_array( 'badges', $meta['visibility'], true ) ) {
733 - $arg['p_sale'] = Fns::get_sale_badge(
1017 + $badge_module = ! empty( $meta['enable_badges_module'] );
1018 + $arg['p_sale'] = Fns::is_module_active( 'product_badges' ) && $badge_module ? 'rtsb_yes' : Fns::get_promo_badge(
1019 + $product,
734 1020 $meta['sale_badge_type'],
735 1021 $meta['sale_badge_text'],
736 - $meta['out_of_stock_badge_text']
1022 + apply_filters( 'rtsb/elementor/render/out_of_stock_badge_text', $meta['out_of_stock_badge_text'] )
737 1023 );
738 1024 }
739 1025
740 1026 $arg['badge_class'] = RenderHelpers::badge_class( $meta['badge_preset'] );
741 1027
1028 + $meta['add_to_cart_text'] = apply_filters( 'rtsb/elementor/render/add_to_cart_text', $meta['add_to_cart_text'], $product );
1029 +
742 1030 if ( ! $meta['show_cart_text'] ) {
743 1031 $meta['add_to_cart_text'] = '';
744 1032 $meta['add_to_cart_success_text'] = '';
745 1033 }
@@ -752,23 +1040,41 @@
752 1040 if ( ! empty( $meta['add_to_cart_success_icon'] ) ) {
753 1041 $cart_icon_html .= Fns::icons_manager( $meta['add_to_cart_success_icon'], 'cart-success-icon' );
754 1042 }
755 1043
756 - $cart_icon = '<span class="icon">' . $cart_icon_html . '</span>';
1044 + $cart_icon = ! empty( $cart_icon_html ) ? '<span class="icon">' . $cart_icon_html . '</span>' : '';
757 1045
758 1046 if ( $product->is_purchasable() || 'grouped' === $p_type || 'external' === $p_type ) {
759 1047 if ( $product->is_in_stock() ) {
760 1048 $arg['add_to_cart'] = $this->render_add_to_cart_button(
761 - get_permalink(),
762 - $post_id,
763 - $p_type,
764 - $meta['add_to_cart_text'],
765 - $meta['add_to_cart_success_text'],
766 - $cart_icon,
767 - $meta['add_to_cart_alignment']
1049 + [
1050 + 'link' => $product->get_permalink(),
1051 + 'id' => $post_id,
1052 + 'type' => $p_type,
1053 + 'text' => $meta['add_to_cart_text'],
1054 + 'success' => $meta['add_to_cart_success_text'],
1055 + 'icon_html' => $cart_icon,
1056 + 'alignment' => $meta['add_to_cart_alignment'],
1057 + 'product' => $product,
1058 + ]
768 1059 );
769 1060 }
770 1061 }
1062 + if ( Fns::is_catalog_mode() && empty( $arg['add_to_cart'] ) ) {
1063 + if ( ! empty( $meta['add_to_cart_icon'] ) ) {
1064 + $cart_icon_html = Fns::icons_manager( $meta['add_to_cart_icon'], 'cart-icon' );
1065 + }
1066 + $cart_icon = ! empty( $cart_icon_html ) ? '<span class="icon">' . $cart_icon_html . '</span>' : '';
1067 + $arg['add_to_cart'] = CatalogModeFns::render_elementor_widget_catalog_mode_button(
1068 + [
1069 + 'id' => $post_id,
1070 + 'type' => $p_type,
1071 + 'text' => $meta['add_to_cart_text'],
1072 + 'icon_html' => $cart_icon,
1073 + 'alignment' => $meta['add_to_cart_alignment'],
1074 + ]
1075 + );
1076 + }
771 1077 }
772 1078
773 1079 if ( in_array( 'quick_view', $meta['visibility'], true ) ) {
774 1080 add_filter( 'rtsb/module/quick_view/button_params', [ $this, 'button_classes' ] );
@@ -784,8 +1090,26 @@
784 1090 add_filter( 'rtsb/module/wishlist/button_params', [ $this, 'button_classes' ] );
785 1091 add_filter( 'rtsb/module/wishlist/icon_html', [ $this, 'wishlist_icon' ] );
786 1092 }
787 1093
1094 + // Eager-load the above-the-fold product images so the LCP candidate is
1095 + // fetched immediately instead of being lazy-loaded after render-blocking
1096 + // CSS and layout. Defaults to the mobile viewport row count (Lighthouse
1097 + // scores mobile); eagering below-the-fold images would only steal
1098 + // bandwidth from the visible ones. Filterable per layout.
1099 + $eager_count = (int) apply_filters( 'rtsb/loop/eager_image_count', 6, $meta );
1100 +
1101 + // The mobile LCP is often NOT the first card but the top-right one in a
1102 + // 2-column grid, so the first *row* of images gets fetchpriority=high to
1103 + // promote the real LCP candidate ahead of render-blocking CSS. Kept small
1104 + // (default 2 = one mobile row) to avoid competing high-priority requests.
1105 + $high_count = (int) apply_filters( 'rtsb/loop/fetchpriority_high_count', 2, $meta );
1106 + $img_priority = false;
1107 +
1108 + if ( $i <= $eager_count ) {
1109 + $img_priority = $i <= $high_count ? 'high' : 'eager';
1110 + }
1111 +
788 1112 $arg['img_html'] = $meta['f_img'] ? Fns::get_product_image_html(
789 1113 'product',
790 1114 $post_id,
791 1115 $meta['f_img_size'],
@@ -790,9 +1114,13 @@
790 1114 $post_id,
791 1115 $meta['f_img_size'],
792 1116 $meta['default_img_id'],
793 1117 $meta['custom_img_size'],
794 - $lazy_load
1118 + $lazy_load,
1119 + false,
1120 + false,
1121 + 0,
1122 + $img_priority
795 1123 ) : null;
796 1124
797 1125 if ( ! ( function_exists( 'rtwpvsp' ) && $product->is_type( 'variable' ) && $product->get_attributes() ) ) {
798 1126 $gallery_ids = $product->get_gallery_image_ids();
@@ -831,249 +1159,470 @@
831 1159 'hover_img_html' => $arg['hover_img_html'],
832 1160 'meta' => $meta,
833 1161 ];
834 1162
835 - return apply_filters( 'rtsb/elementor/render/arg_dataset', $arg, $post_id, $meta, $lazy_load );
1163 + return $arg;
836 1164 }
837 -
838 1165 /**
839 - * Builds an array with meta values.
1166 + * Renders add to cart button.
840 1167 *
841 - * @param array $meta Meta values.
842 - * @param Object $query Category object.
843 - * @param bool $fullwidth Fullwidth check.
844 - * @param bool $lazy_load Lazy Load.
1168 + * @param array $args Cart args.
845 1169 *
846 - * @return array
1170 + * @return string
847 1171 */
848 - public function cat_arg_dataset( $meta, $query = null, $fullwidth = false, $lazy_load = false, $category_loop = false ) {
849 - if ( empty( $meta ) ) {
850 - return [];
851 - }
1172 + public function render_add_to_cart_button( $args ) {
1173 + list(
1174 + 'link' => $link,
1175 + 'id' => $id,
1176 + 'type' => $type,
1177 + 'text' => $text,
1178 + 'success' => $success,
1179 + 'icon_html' => $icon_html,
1180 + 'alignment' => $alignment,
1181 + 'product' => $product
1182 + ) = $args;
852 1183
853 - $arg = [];
1184 + $content = null;
1185 + $rand = wp_rand();
1186 + $ext_link = get_post_meta( $id, '_product_url', true );
1187 + $btn_txt = get_post_meta( $id, '_button_text', true );
854 1188
855 - $products = null;
856 - $arg['class'] = null;
857 - $arg['grid'] = null;
858 - $arg['anchor_class'] = null;
859 - $arg['p_sale'] = null;
860 - $arg['count'] = max( $query->count, 0 );
861 - $arg['count_position'] = $meta['count_position'];
862 - $arg['excerpt_position'] = $meta['cat_excerpt_position'];
863 - $arg['cat_link'] = get_term_link( $query );
864 - $arg['target'] = '_self';
865 - $arg['raw_settings'] = ! empty( $meta['raw_settings'] ) ? $meta['raw_settings'] : [];
1189 + ob_start();
1190 + do_action( 'rtsb/before/cart/button' );
1191 + $content .= ob_get_clean();
866 1192
867 - if ( ! empty( $meta['tag_term'] ) ) {
868 - $arg['tag_term'] = $meta['tag_term'];
1193 + $cart_attr = [
1194 + 'class' => 'rtsb-action-btn ' . $type . '-product tipsy icon-' . $alignment . ' ' . ( empty( $text ) ? 'no-text' : 'has-text' ),
1195 + 'href' => esc_url( $link ),
1196 + 'data-quantity' => '1',
1197 + 'data-product_id' => absint( $id ),
1198 + 'data-id' => absint( $id ),
1199 + ];
1200 +
1201 + if ( empty( $text ) ) {
1202 + $tooltip_mapping = [
1203 + 'simple' => esc_attr__( 'Add to Cart', 'shopbuilder' ),
1204 + 'variable' => esc_attr__( 'Select Options', 'shopbuilder' ),
1205 + 'grouped' => esc_attr__( 'View Products', 'shopbuilder' ),
1206 + 'external' => ! empty( $btn_txt ) ? esc_html( $btn_txt ) : esc_html__( 'Buy Product', 'shopbuilder' ),
1207 + 'rtsb-gift-card' => esc_attr__( 'Select Gift Card', 'shopbuilder' ),
1208 + ];
1209 +
1210 + $cart_attr['title'] = $tooltip_mapping[ $type ];
869 1211 }
870 1212
871 - if ( ! empty( $meta['custom_link']['url'] ) ) {
872 - $arg['cat_link'] = $meta['custom_link']['url'];
873 - $arg['target'] = $meta['custom_link']['is_external'] ? '_blank' : '_self';
1213 + $content .= '<div class="rtsb-wc-add-to-cart-wrap">';
1214 + if ( 'variable' === $type ) {
1215 + $cart_attr['class'] = $cart_attr['class'] . ' rtsb_vs_add_to_cart ';
874 1216 }
1217 + if ( 'variable' === $type || 'grouped' === $type ) {
1218 + $cart_attr = apply_filters( 'rtsb/elementor/render/cart_attributes', $cart_attr, $product );
875 1219
876 - $title = $query->name;
877 - $excerpt = $query->description;
1220 + $this->add_attribute( 'rtsb_add_to_cart_button_' . $id . $rand, $cart_attr );
878 1221
879 - if ( in_array( 'badges', $meta['visibility'], true ) ) {
880 - $arg['p_sale'] = $meta['custom_badge_text'];
1222 + $text_btn = apply_filters(
1223 + 'rtsb/elementor/render/add_to_cart_text',
1224 + 'grouped' === $type ? __( 'View Products', 'shopbuilder' ) : __( 'Select Options', 'shopbuilder' ),
1225 + $product,
1226 + $type
1227 + );
1228 +
1229 + $content .= '<a ' . $this->get_attribute_string( 'rtsb_add_to_cart_button_' . $id . $rand ) . '>';
1230 + $content .= $icon_html;
1231 + $content .= '<span class="button-text text ' . ( ! empty( $success ) ? 'has-success-text' : 'no-success-text' ) . '" data-success="' . esc_html( $success ) . '" data-cart-text="' . ( ! empty( $text ) ? esc_attr( $text ) : $tooltip_mapping['simple'] ) . '" data-variable-text="' . esc_attr( $text_btn ) . '">' . esc_html( $text_btn ) . '</span>';
1232 + } elseif ( 'external' === $type ) {
1233 + if ( ! empty( $ext_link ) ) {
1234 + $cart_attr['target'] = '_blank';
1235 + }
1236 +
1237 + $cart_attr['href'] = ! empty( $ext_link ) ? esc_url( $ext_link ) : esc_url( $link );
1238 +
1239 + $this->add_attribute( 'rtsb_add_to_cart_button_' . $id . $rand, $cart_attr );
1240 +
1241 + $content .= '<a ' . $this->get_attribute_string( 'rtsb_add_to_cart_button_' . $id . $rand ) . '>';
1242 + $content .= $icon_html;
1243 + $content .= '<span class="button-text text">' . ( ! empty( $btn_txt ) ? esc_html( $btn_txt ) : esc_html__( 'Buy Product', 'shopbuilder' ) ) . '</span>';
1244 + } else {
1245 + $cart_attr['href'] = $cart_attr['href'] . '?add-to-cart=' . absint( $id );
1246 + $cart_attr['class'] .= ' rtsb-add-to-cart-btn';
1247 + $cart_attr['data-type'] = $type;
1248 + $cart_attr = apply_filters( 'rtsb/elementor/render/cart_attributes', $cart_attr, $product );
1249 +
1250 + $this->add_attribute( 'rtsb_add_to_cart_button_' . $id . $rand, $cart_attr );
1251 +
1252 + $content .= '<a ' . $this->get_attribute_string( 'rtsb_add_to_cart_button_' . $id . $rand ) . '>';
1253 + $content .= $icon_html;
1254 + $content .= '<span class="button-text text ' . ( ! empty( $success ) ? 'has-success-text' : 'no-success-text' ) . '" data-success="' . esc_html( $success ) . '">' . esc_html( $text ) . '</span>';
881 1255 }
882 1256
883 - $arg['badge_class'] = RenderHelpers::badge_class( $meta['badge_preset'] );
1257 + $content .= '</a>';
1258 + $content .= '</div>';
884 1259
885 - if ( $arg['count'] >= 0 ) {
886 - $prefix = ! empty( $meta['before_count'] ) ? $meta['before_count'] : '';
887 - $suffix = ! empty( $meta['after_count'] ) ? $meta['after_count'] : '';
1260 + ob_start();
1261 + do_action( 'rtsb/after/cart/button' );
1262 + $content .= ob_get_clean();
888 1263
889 - $arg['count'] = $prefix . $arg['count'] . $suffix;
1264 + return apply_filters( 'rtsb/render/cart/button', $content, $args );
1265 + }
1266 +
1267 + /**
1268 + * Button classes.
1269 + *
1270 + * @param array $params Button params.
1271 + *
1272 + * @return array
1273 + */
1274 + public function button_classes( $params ) {
1275 + $params['classes'] = array_merge( $params['classes'], [ 'rtsb-action-btn', 'tipsy' ] );
1276 +
1277 + return $params;
1278 + }
1279 +
1280 + /**
1281 + * Render the section title.
1282 + *
1283 + * @param array $settings The Elementor settings.
1284 + *
1285 + * @return string
1286 + */
1287 + public function render_section_title( $settings ) {
1288 + $html = '';
1289 + $rand = wp_rand();
1290 +
1291 + if ( empty( $settings['show_section_title'] ) || ( empty( $settings['query_products'] ) && 0 === count( $settings['query_products'] ) ) ) {
1292 + return $html;
890 1293 }
891 1294
892 - if ( $meta['show_custom_title'] && ! empty( $meta['cat_custom_title'] ) ) {
893 - $title = $meta['cat_custom_title'];
894 - }
1295 + // Set attributes for the section title.
1296 + $this->add_attribute( 'rtsb_section_title_wrapper_' . $rand, 'class', 'rtsb-section-title-wrapper' );
1297 + $this->add_attribute( 'rtsb_section_title_' . $rand, 'class', 'rtsb-section-title' );
895 1298
896 - if ( $meta['show_custom_excerpt'] && ! empty( $meta['cat_custom_excerpt'] ) ) {
897 - $excerpt = $meta['cat_custom_excerpt'];
1299 + // Start rendering.
1300 + $html .= '<div ' . $this->get_attribute_string( 'rtsb_section_title_wrapper_' . $rand ) . '>';
1301 + $html .= '<' . esc_attr( $settings['section_title_tag'] ) . ' ' . $this->get_attribute_string( 'rtsb_section_title_' . $rand ) . '>';
1302 + $html .= esc_html( $settings['section_title_text'] );
1303 + $html .= '</' . esc_attr( $settings['section_title_tag'] ) . '>';
1304 + $html .= '</div>';
1305 +
1306 + return $html;
1307 + }
1308 +
1309 + /**
1310 + * Renders products pagination
1311 + *
1312 + * @param array $args Pagination args.
1313 + * @param string $query_type Query type.
1314 + *
1315 + * @return string
1316 + */
1317 + public function render_products_pagination( $args, $query_type = 'wc' ) {
1318 + list(
1319 + 'query' => $query,
1320 + 'meta' => $meta,
1321 + 'limit' => $limit,
1322 + 'per_page' => $per_page
1323 + ) = $args;
1324 +
1325 + $html_utility = null;
1326 + $html = null;
1327 + $ajax = false;
1328 + $is_grid = preg_match( '/grid-layout/', $meta['layout'] ) || preg_match( '/list-layout/', $meta['layout'] );
1329 + $posts_per_page = 'wp' === $query_type ? $query->query_vars['posts_per_page'] : null;
1330 + $total_page = 'wp' === $query_type ? $query->max_num_pages : $query->get_products()->max_num_pages;
1331 +
1332 + if ( $limit ) {
1333 + if ( 'wc' === $query_type ) {
1334 + $found_post = $query->get_products()->total;
1335 + } elseif ( 'wp' === $query_type && ( empty( $wp_query->query['tax_query'] ) ) ) {
1336 + $found_post = $query->found_posts;
1337 + }
1338 +
1339 + if ( $per_page && $found_post > $per_page ) {
1340 + $found_post = $limit;
1341 + $total_page = ceil( $found_post / $per_page );
1342 + }
898 1343 }
899 1344
900 - $layout = $meta['layout'];
1345 + $total_page = absint( $total_page );
901 1346
902 - $arg['c_img'] = $meta['c_img'];
903 - $arg['items'] = $meta['visibility'];
904 - $arg['title_tag'] = $meta['title_tag'];
905 - $arg['title_class'] = 'category-title rtsb-text-limit limit-' . $meta['title_limit'] . ( ! $meta['title_link'] ? ' no-link' : '' );
906 - $arg['title'] = Fns::text_truncation( $title, $meta['title_limit_custom'] );
907 - $arg['excerpt_limit'] = $meta['excerpt_limit'];
908 - $arg['excerpt'] = wpautop( Fns::text_truncation( $excerpt, $meta['excerpt_limit_custom'] ) );
909 - $arg['title_link'] = $meta['title_link'];
910 - $arg['image_link'] = $meta['image_link'];
1347 + $args = [
1348 + 'total_page' => $total_page,
1349 + 'posts_per_page' => 'wc' === $query_type ? $per_page : $posts_per_page,
1350 + 'ajax' => $ajax,
1351 + ];
911 1352
912 - if ( ! $fullwidth ) {
913 - $arg['grid'] .= 'rtsb-col-grid ';
914 - $arg['class'] .= ' even-grid-item ';
1353 + if ( 'pagination' === $meta['posts_loading_type'] ) {
1354 + if ( $is_grid ) {
1355 + $html_utility .= Fns::custom_pagination(
1356 + $total_page,
1357 + 'wc' === $query_type ? $per_page : $posts_per_page,
1358 + false,
1359 + $meta['visible_range'] ?? null
1360 + );
1361 + }
1362 + } elseif ( 'pagination_ajax' === $meta['posts_loading_type'] && ! rtsb()->has_pro() ) {
1363 + // Free version fallback: render non-AJAX number pagination so filter
1364 + // params in the URL are preserved via get_pagenum_link().
1365 + if ( $is_grid ) {
1366 + $html_utility .= Fns::custom_pagination(
1367 + $total_page,
1368 + 'wc' === $query_type ? $per_page : $posts_per_page,
1369 + false,
1370 + $meta['visible_range'] ?? null
1371 + );
1372 + }
915 1373 } else {
916 - $arg['grid'] .= 'rtsb-col-grid ';
1374 + ob_start();
1375 + do_action( 'rtsb/elementor/render/pagination', $meta, $query, $args );
1376 + $html_utility .= ob_get_clean();
917 1377 }
918 1378
919 - $arg['class'] .= 'rtsb-category-grid';
1379 + if ( $html_utility ) {
1380 + $rand = wp_rand();
920 1381
921 - $arg['img_html'] = $meta['c_img'] ? Fns::get_product_image_html(
922 - 'category',
923 - $query->term_id,
924 - $meta['f_img_size'],
925 - $meta['default_img_id'],
926 - $meta['custom_img_size'],
927 - $lazy_load
928 - ) : null;
1382 + // Adding pagination render attributes.
1383 + $this->add_attribute(
1384 + 'rtsb_pagination_attr_' . $rand,
1385 + [
1386 + 'class' => 'rtsb-pagination-wrap element-loading',
1387 + 'data-total-pages' => $total_page,
1388 + 'data-posts-per-page' => 'wc' === $query_type ? $per_page : $posts_per_page,
1389 + 'data-type' => $meta['posts_loading_type'],
1390 + ]
1391 + );
929 1392
930 - if ( $meta['show_custom_image'] && ! empty( $meta['custom_image'] ) ) {
931 - $arg['img_html'] = Fns::get_product_image_html(
932 - 'custom',
933 - null,
934 - $meta['f_img_size'],
935 - $meta['custom_image'],
936 - $meta['custom_img_size'],
937 - $lazy_load
938 - );
1393 + // Start rendering.
1394 + $html = '<div ' . $this->get_attribute_string( 'rtsb_pagination_attr_' . $rand ) . '>';
1395 + $html .= $html_utility;
1396 + $html .= '</div><!-- .rtsb-pagination-wrap -->';
939 1397 }
940 1398
941 - $arg['excerpt_class'] = 'category-info' . ( 'block' === $meta['count_position'] ? ' block-count' : ' inline-count' ) . ( 'above' === $meta['cat_excerpt_position'] ? ' excerpt-above' : '' ) . ( empty( $arg['excerpt'] ) ? ' no-excerpt' : '' );
1399 + return $html;
1400 + }
942 1401
943 - if ( $category_loop ) {
944 - $query_args = [
945 - 'product_category_id' => $query->term_id,
946 - 'limit' => -1,
947 - 'order' => 'DESC',
948 - 'orderby' => 'date',
949 - ];
1402 + /**
1403 + * Renders slider buttons
1404 + *
1405 + * @param array $settings Settings array.
1406 + *
1407 + * @return string
1408 + */
1409 + public function render_slider_buttons( $settings ) {
1410 + $html = '';
1411 + $left_icon = $settings['left_arrow_icon'];
1412 + $right_icon = $settings['right_arrow_icon'];
950 1413
951 - $products_query = new \WC_Product_Query( $query_args );
952 - $products = $products_query->get_products();
1414 + if ( ! empty( $settings['slider_nav'] ) ) {
1415 + $html .=
1416 + '<div class="swiper-nav">
1417 + <div class="swiper-arrow swiper-button-next">
1418 + ' . Fns::icons_manager( $right_icon ) . '
1419 + </div>
1420 + <div class="swiper-arrow swiper-button-prev">
1421 + ' . Fns::icons_manager( $left_icon ) . '
1422 + </div>
1423 + </div>';
953 1424 }
954 1425
955 - return apply_filters( 'rtsb/elementor/render/cat_arg_dataset', $arg, $meta, $query, $fullwidth, $lazy_load, $products );
1426 + $html .= ! empty( $settings['slider_dot'] ) ? '<div class="swiper-pagination rtsb-pos-s"></div>' : '';
1427 +
1428 + return $html;
956 1429 }
957 1430
958 1431 /**
959 - * Renders add to cart button.
1432 + * No products message.
960 1433 *
961 - * @param string $link Product link.
962 - * @param int $id Product ID.
963 - * @param string $type Product type.
964 - * @param string $text Custom text.
965 - * @param string $success Success text.
966 - * @param string $icon_html Custom icon.
967 - * @param string $alignment Icon alignment.
1434 + * @param array $metas Meta data.
968 1435 *
1436 + * @return void
1437 + */
1438 + public function no_products_msg( $metas ) {
1439 + $content =
1440 + '<div class="rtsb-notice woocommerce-no-products-found">
1441 + <div class="woocommerce-info">' . esc_html__( 'No products were found matching your selection.', 'shopbuilder' ) . '</div>
1442 + </div>';
1443 +
1444 + $this->row( $content, $metas );
1445 + }
1446 +
1447 + /**
1448 + * Start the rendering.
1449 + *
1450 + * @param array $metas Meta Data.
1451 + * @param array $settings Widget settings.
1452 + * @param array $temp_args Additional temporary arguments for rendering.
1453 + *
969 1454 * @return string
970 1455 */
971 - public function render_add_to_cart_button( $link, $id, $type, $text, $success, $icon_html, $alignment ) {
972 - $content = null;
973 - $tooltip_attr = '';
974 - $ext_link = get_post_meta( $id, '_product_url', true );
975 - $btn_txt = get_post_meta( $id, '_button_text', true );
1456 + public function render_start( $metas, $settings, $temp_args ) {
1457 + ob_start();
976 1458
977 - if ( empty( $text ) ) {
978 - $tooltip_attr = ' title="' . esc_attr__( 'Add to Cart', 'shopbuilder' ) . '"';
1459 + /**
1460 + * Before render row hook.
1461 + */
1462 + do_action( 'rtsb/elementor/render/before_row', $metas, $settings, $temp_args, $this );
979 1463
980 - if ( 'variable' === $type ) {
981 - $tooltip_attr = ' title="' . esc_attr__( 'Select Options', 'shopbuilder' ) . '"';
1464 + return ob_get_clean();
1465 + }
1466 +
1467 + /**
1468 + * End the rendering.
1469 + *
1470 + * @param array $metas Meta Data.
1471 + * @param array $settings Widget settings.
1472 + * @param array $products Array of products being rendered.
1473 + *
1474 + * @return string
1475 + */
1476 + public function render_end( $metas, $settings, $products ) {
1477 + ob_start();
1478 +
1479 + /**
1480 + * After render row hook.
1481 + */
1482 + do_action( 'rtsb/elementor/render/after_row', $metas, $settings, $products );
1483 +
1484 + return ob_get_clean();
1485 + }
1486 +
1487 + /**
1488 + * Add render attribute.
1489 + *
1490 + * @param array|string $element The HTML element.
1491 + * @param array|string $key Optional. Attribute key. Default is null.
1492 + * @param array|string $value Optional. Attribute value. Default is null.
1493 + * @param bool $overwrite Optional. Whether to overwrite existing.
1494 + *
1495 + * @return self
1496 + */
1497 + public function add_attribute( $element, $key = null, $value = null, $overwrite = false ) {
1498 + if ( is_array( $element ) ) {
1499 + foreach ( $element as $element_key => $attributes ) {
1500 + $this->add_attribute( $element_key, $attributes, null, $overwrite );
982 1501 }
983 1502
984 - if ( 'grouped' === $type ) {
985 - $tooltip_attr = ' title="' . esc_attr__( 'View Products', 'shopbuilder' ) . '"';
1503 + return $this;
1504 + }
1505 +
1506 + if ( is_array( $key ) ) {
1507 + foreach ( $key as $attribute_key => $attributes ) {
1508 + $this->add_attribute( $element, $attribute_key, $attributes, $overwrite );
986 1509 }
987 1510
988 - if ( 'external' === $type ) {
989 - $tooltip_txt = ! empty( $btn_txt ) ? esc_html( $btn_txt ) : esc_html__( 'Buy Product', 'shopbuilder' );
990 - $tooltip_attr = ' title="' . esc_attr( $tooltip_txt ) . '"';
991 - }
1511 + return $this;
992 1512 }
993 1513
994 - ob_start();
995 - do_action( 'rtsb/before/cart/button' );
996 - $content .= ob_get_clean();
1514 + if ( empty( $this->attributes[ $element ][ $key ] ) ) {
1515 + $this->attributes[ $element ][ $key ] = [];
1516 + }
997 1517
998 - if ( 'variable' === $type ) {
999 - $data_cart_text = ! empty( $text ) ? esc_attr( $text ) : __( 'Add to Cart', 'shopbuilder' );
1000 - $variable_text = __( 'Select Options', 'shopbuilder' );
1518 + settype( $value, 'array' );
1001 1519
1002 - $content .= sprintf(
1003 - '<div class="rtsb-wc-add-to-cart-wrap">
1004 - <a href="%1$s" data-quantity="1" class="rtsb-action-btn variable-product tipsy icon-%4$s %5$s" data-id="%3$d" data-product_id="%3$s" rel="nofollow"%6$s>%7$s<span class="text" data-cart-text="%8$s" data-variable-text="%9$s" data-success="%10$s">%2$s</span><span></span></a>
1005 - </div>',
1006 - esc_url( $link ),
1007 - esc_html__( 'Select Options', 'shopbuilder' ),
1008 - absint( $id ),
1009 - esc_attr( $alignment ),
1010 - empty( $text ) ? 'no-text' : 'has-text',
1011 - $tooltip_attr,
1012 - $icon_html,
1013 - esc_attr( $data_cart_text ),
1014 - esc_attr( $variable_text ),
1015 - esc_html( $success ),
1016 - );
1017 - } elseif ( 'grouped' === $type ) {
1018 - $content .= sprintf(
1019 - '<div class="rtsb-wc-add-to-cart-wrap">
1020 - <a href="%1$s" data-quantity="1" class="rtsb-action-btn variable-product tipsy icon-%4$s %5$s" data-product_id="%3$s" rel="nofollow"%6$s>%7$s<span class="text">%2$s</span></a>
1021 - </div>',
1022 - esc_url( $link ),
1023 - esc_html__( 'View Products', 'shopbuilder' ),
1024 - absint( $id ),
1025 - esc_attr( $alignment ),
1026 - empty( $text ) ? 'no-text' : 'has-text',
1027 - $tooltip_attr,
1028 - $icon_html,
1029 - );
1030 - } elseif ( 'external' === $type ) {
1031 - $content .= sprintf(
1032 - '<div class="rtsb-wc-add-to-cart-wrap">
1033 - <a href="%1$s" data-quantity="1" class="rtsb-action-btn variable-product tipsy icon-%4$s %5$s" data-product_id="%3$s" rel="nofollow"%6$s %8$s>%7$s<span class="text">%2$s</span></a>
1034 - </div>',
1035 - ! empty( $ext_link ) ? esc_url( $ext_link ) : esc_url( $link ),
1036 - ! empty( $btn_txt ) ? esc_html( $btn_txt ) : esc_html__( 'Buy Product', 'shopbuilder' ),
1037 - absint( $id ),
1038 - esc_attr( $alignment ),
1039 - empty( $text ) ? 'no-text' : 'has-text',
1040 - $tooltip_attr,
1041 - $icon_html,
1042 - ! empty( $ext_link ) ? ' target="_blank"' : ''
1043 - );
1520 + if ( $overwrite ) {
1521 + $this->attributes[ $element ][ $key ] = $value;
1044 1522 } else {
1045 - $content .= sprintf(
1046 - '<a href="%1$s?add-to-cart=%2$d" data-quantity="1" class="rtsb-action-btn rtsb-add-to-cart-btn tipsy icon-%6$s %7$s" data-id="%2$d" data-type="%3$s"%8$s>%4$s<span class="text %10$s" data-success="%9$s">%5$s</span><span></span></a>',
1047 - esc_url( $link ),
1048 - absint( $id ),
1049 - esc_attr( $type ),
1050 - $icon_html,
1051 - esc_html( $text ),
1052 - esc_attr( $alignment ),
1053 - empty( $text ) ? 'no-text' : 'has-text',
1054 - $tooltip_attr,
1055 - esc_html( $success ),
1056 - ! empty( $success ) ? 'has-success-text' : 'no-success-text',
1057 - );
1523 + $this->attributes[ $element ][ $key ] = array_merge( $this->attributes[ $element ][ $key ], $value );
1058 1524 }
1059 1525
1060 - ob_start();
1061 - do_action( 'rtsb/after/cart/button' );
1062 - $content .= ob_get_clean();
1526 + return $this;
1527 + }
1063 1528
1064 - return $content;
1529 + /**
1530 + * Add link render attributes.
1531 + *
1532 + * @param array|string $element The HTML element.
1533 + * @param array $url_control Array of link settings.
1534 + * @param bool $overwrite Optional. Whether to overwrite existing
1535 + * attribute. Default is false, not to overwrite.
1536 + *
1537 + * @return Render
1538 + */
1539 + public function add_link_attributes( $element, array $url_control, $overwrite = false ) {
1540 + $attributes = [];
1541 +
1542 + if ( ! empty( $url_control['url'] ) ) {
1543 + $allowed_protocols = array_merge( wp_allowed_protocols(), [ 'skype', 'viber' ] );
1544 +
1545 + $attributes['href'] = esc_url( $url_control['url'], $allowed_protocols );
1546 + }
1547 +
1548 + if ( ! empty( $url_control['is_external'] ) ) {
1549 + $attributes['target'] = '_blank';
1550 + }
1551 +
1552 + if ( ! empty( $url_control['nofollow'] ) ) {
1553 + $attributes['rel'] = 'nofollow';
1554 + }
1555 +
1556 + if ( ! empty( $url_control['custom_attributes'] ) ) {
1557 + $attributes = array_merge( $attributes, Utils::parse_custom_attributes( $url_control['custom_attributes'] ) );
1558 + }
1559 +
1560 + if ( $attributes ) {
1561 + $this->add_attribute( $element, $attributes, null, $overwrite );
1562 + }
1563 +
1564 + return $this;
1065 1565 }
1066 1566
1067 1567 /**
1068 - * Button classes.
1568 + * Get render attribute string.
1069 1569 *
1070 - * @param array $params Button params.
1570 + * @param string $element The element.
1071 1571 *
1072 - * @return array
1572 + * @return string
1073 1573 */
1074 - public function button_classes( $params ) {
1075 - $params['classes'] = array_merge( $params['classes'], [ 'rtsb-action-btn', 'tipsy' ] );
1574 + public function get_attribute_string( $element ) {
1575 + if ( empty( $this->attributes[ $element ] ) ) {
1576 + return '';
1577 + }
1076 1578
1077 - return $params;
1579 + if ( class_exists( '\Elementor\Utils' ) ) {
1580 + return Utils::render_html_attributes( $this->attributes[ $element ] );
1581 + }
1582 +
1583 + // Fallback.
1584 + return $this->render_fallback_attributes( $this->attributes[ $element ] );
1585 + }
1586 +
1587 + /**
1588 + * Function to render link attributes.
1589 + *
1590 + * @param string $id Element ID.
1591 + * @param array $control_name Control name.
1592 + * @param string $class_name Class name.
1593 + *
1594 + * @return string
1595 + */
1596 + public function render_link_attributes( $id, $control_name, $class_name = 'logo-img-link' ) {
1597 + $this->add_attribute( $id, 'class', $class_name );
1598 +
1599 + if ( ! empty( $control_name['url'] ) ) {
1600 + $this->add_link_attributes( $id, $control_name );
1601 + } else {
1602 + $this->add_attribute( $id, 'role', 'button' );
1603 + }
1604 +
1605 + return $this->get_attribute_string( $id );
1606 + }
1607 +
1608 + /**
1609 + * Render fallback attributes.
1610 + *
1611 + * @param array $attributes The attributes.
1612 + *
1613 + * @return string
1614 + */
1615 + private function render_fallback_attributes( $attributes ) {
1616 + $rendered_attributes = [];
1617 +
1618 + foreach ( $attributes as $attribute_key => $attribute_values ) {
1619 + if ( is_array( $attribute_values ) ) {
1620 + $attribute_values = implode( ' ', $attribute_values );
1621 + }
1622 +
1623 + $rendered_attributes[] = sprintf( '%1$s="%2$s"', $attribute_key, esc_attr( $attribute_values ) );
1624 + }
1625 +
1626 + return implode( ' ', $rendered_attributes );
1078 1627 }
1079 1628 }