PluginProbe
Ultimate Store Kit – Store Builder Addons for Elementor, WooCommerce Store Builder, EDD Store Builder / 3.1.4
Ultimate Store Kit – Store Builder Addons for Elementor, WooCommerce Store Builder, EDD Store Builder v3.1.4
3.1.4 3.0.8 3.0.9 3.1.0 3.1.2 3.1.3 3.0.7 3.0.5 3.0.4 3.0.3 3.0.2 trunk 1.5.0 1.5.1 1.5.2 1.6.1 1.6.2 1.6.3 1.6.4 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 All 93 releases
ultimate-store-kit / templates / shiny-grid.php

shiny-grid.php in Ultimate Store Kit – Store Builder Addons for Elementor, WooCommerce Store Builder, EDD Store Builder 3.1.4, at templates/shiny-grid.php

532 lines 22.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Shiny Grid Template Class
5 *
6 * Handles rendering of product items in the Shiny Grid layout
7 */
8
9 namespace UltimateStoreKit\Templates;
10
11 if (! defined('ABSPATH')) {
12 exit; // Exit if accessed directly
13 }
14
15 // phpcs:disable WordPress.NamingConventions.PrefixAllGlobals -- usk_ / BDTUSK_ / ultimate-store-kit- are this plugin's established public prefixes. Ultimate Store Kit Pro calls into these names, as does third-party integration code, so renaming them is a breaking change. Plugin Check only recognises prefixes derived verbatim from the slug and so reports them as unprefixed.
16
17 use UltimateStoreKit\Traits\Global_Widget_Template;
18 use UltimateStoreKit\Classes\Utils;
19
20 class USK_Shiny_Grid_Template {
21 use Global_Widget_Template;
22
23 /**
24 * Widget settings and name
25 */
26 protected $target_widget_settings = [];
27 protected $target_widget_name = '';
28
29 /**
30 * Constructor
31 */
32 public function __construct($settings = [], $widget_name = '') {
33 $this->target_widget_settings = $settings;
34 $this->target_widget_name = $widget_name;
35 }
36
37 /**
38 * Render a single product item
39 */
40 public function render_shiny_grid_item($product, $settings) {
41 if (!$product) {
42 return;
43 }
44
45 $product_id = $product->get_id();
46 $rating_count = $product->get_rating_count();
47 $average = $product->get_average_rating();
48 $categories = wc_get_product_category_list($product_id, ' ');
49
50 $show_rating = isset($settings['show_rating']) ? $settings['show_rating'] : true;
51 $classes = $this->target_widget_name === 'shiny-grid' ? 'usk-item' : 'usk-item swiper-slide';
52 $classes .= $show_rating ? ' usk-have-rating' : '';
53 ?>
54 <div class="<?php echo esc_attr($classes); ?>" data-product-id="<?php echo esc_attr($product_id); ?>">
55 <div class="usk-item-box">
56 <?php $this->render_product_image($product); ?>
57 <div class="usk-content">
58 <?php if (isset($settings['show_variation']) && $settings['show_variation'] === 'yes'): ?>
59 <?php $this->render_product_variation($product, $settings);
60 ?>
61 <?php endif; ?>
62 <div class="usk-content-inner">
63 <?php if ($categories && (isset($settings['show_category']) ? $settings['show_category'] : true)): ?>
64 <div class="usk-category"><?php echo wp_kses_post($categories); ?></div>
65 <?php endif; ?>
66
67 <?php if (isset($settings['show_title']) ? $settings['show_title'] : true) :
68 printf(
69 '<%1$s class="title"><a href="%2$s" class="usk-title" aria-label="%4$s">%3$s</a></%1$s>',
70 esc_attr(Utils::get_valid_html_tag(isset($settings['title_tags']) ? $settings['title_tags'] : 'h3')),
71 esc_url($product->get_permalink()),
72 esc_html($product->get_title()),
73 esc_attr(sprintf('View details for %s', $product->get_title()))
74 );
75 endif; ?>
76
77 <?php if (
78 (isset($settings['show_excerpt']) ? $settings['show_excerpt'] : true)
79 && isset($settings['layout_style']) && $settings['layout_style'] === 'list'
80 ): ?>
81 <div class="usk-desc">
82 <span class="desc">
83 <?php echo wp_kses_post(wp_trim_words($product->get_short_description(), $settings['excerpt_limit'], '')); ?>
84 </span>
85 </div>
86 <?php endif; ?>
87
88 <?php if (isset($settings['show_price']) ? $settings['show_price'] : true && $product->get_price_html()): ?>
89 <div class="usk-price">
90 <?php $this->print_price_output($product->get_price_html()); ?>
91 </div>
92 <?php endif; ?>
93
94 <?php if (isset($settings['show_rating']) ? $settings['show_rating'] : true): ?>
95 <div class="usk-rating">
96 <span><?php echo wp_kses_post($this->register_global_template_wc_rating($average, $rating_count)); ?></span>
97 </div>
98 <?php endif; ?>
99 </div>
100
101 </div>
102 </div>
103 </div>
104 <?php
105 }
106
107 /**
108 * Render add to cart button
109 */
110 public function __render_add_to_cart_button($product) {
111 if (!$product) {
112 return;
113 }
114
115 // Check if product is variable and has default attributes
116 $has_default_attributes = false;
117 if ($product->is_type('variable')) {
118 $default_attributes = $product->get_default_attributes();
119 if (!empty($default_attributes)) {
120 $has_default_attributes = true;
121 }
122 }
123
124 // Set up button classes
125 $button_classes = [
126 'usk-button',
127 'product_type_' . $product->get_type(),
128 ];
129
130 // Add appropriate classes for purchasable products
131 if ($product->is_purchasable() && $product->is_in_stock() && (!$product->is_type('variable') || $has_default_attributes)) {
132 $button_classes[] = 'add_to_cart_button';
133
134 if ($product->supports('ajax_add_to_cart')) {
135 $button_classes[] = 'ajax_add_to_cart';
136 }
137 }
138
139 // Set up button attributes
140 $button_attributes = [
141 'data-product_id' => $product->get_id(),
142 'data-product_sku' => $product->get_sku(),
143 'aria-label' => $product->add_to_cart_description(),
144 // 'rel' => 'nofollow',
145 ];
146
147 // Add default attributes data for variable products
148 if ($product->is_type('variable') && $has_default_attributes) {
149 $button_attributes['data-default_attributes'] = htmlspecialchars(
150 \wp_json_encode($product->get_default_attributes()),
151 ENT_QUOTES,
152 'UTF-8'
153 );
154
155 $variation_id = $this->get_variation_id_from_attributes($product, $product->get_default_attributes());
156 if ($variation_id) {
157 $button_attributes['data-variation_id'] = $variation_id;
158
159 // Add each default attribute as separate data attributes for AJAX support
160 foreach ($default_attributes as $attribute_name => $attribute_value) {
161 $button_attributes['data-attribute_' . $attribute_name] = $attribute_value;
162 }
163
164 // If we have a default variation, use product_type_variation instead of variable
165 if (in_array('product_type_variable', $button_classes)) {
166 $key = array_search('product_type_variable', $button_classes);
167 if ($key !== false) {
168 $button_classes[$key] = 'product_type_variation';
169 }
170 }
171 }
172 }
173
174 // Create button args
175 $args = [
176 'quantity' => 1,
177 'class' => implode(' ', $button_classes),
178 'attributes' => $button_attributes,
179 ];
180
181 $args = \apply_filters('woocommerce_loop_add_to_cart_args', $args, $product);
182
183 // Clean up aria-label
184 if (isset($args['attributes']['aria-label'])) {
185 $args['attributes']['aria-label'] = \wp_strip_all_tags($args['attributes']['aria-label']);
186 }
187
188 // Determine button text
189 if ($product->is_type('variable')) {
190 $button_text = $has_default_attributes ? \__('Add to cart', 'ultimate-store-kit') : \__('Select options', 'ultimate-store-kit');
191 } else {
192 $button_text = $product->add_to_cart_text();
193 }
194
195 // Get the URL
196 if ($product->is_type('variable')) {
197 $url = 'javascript:void(0)';
198 } else {
199 $url = $product->add_to_cart_url();
200 }
201
202 // Make sure variable products use correct classes
203 if ($product->is_type('variable') && in_array('product_type_variable', $button_classes)) {
204 $key = array_search('product_type_variable', $button_classes);
205 if ($key !== false) {
206 $button_classes[$key] = 'product_type_variation';
207 }
208 }
209
210 // Output the button
211 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Returns markup by contract; the sprintf arguments below are escaped individually, and WooCommerce core echoes this same filter unescaped.
212 echo \apply_filters(
213 'woocommerce_loop_add_to_cart_link',
214 sprintf(
215 '<a href="%s" data-quantity="%s" class="%s" %s>%s <i class="button-icon usk-icon-arrow-right-8"></i></a>',
216 \esc_url($url),
217 \esc_attr(isset($args['quantity']) ? $args['quantity'] : 1),
218 \esc_attr(isset($args['class']) ? $args['class'] : 'button'),
219 isset($args['attributes']) ? \wc_implode_html_attributes($args['attributes']) : '',
220 \esc_html($button_text)
221 ),
222 $product,
223 $args
224 );
225 }
226 public function render_add_to_cart_button($product, $settings) {
227 if ($product) {
228 $defaults = [
229 'quantity' => 1,
230 'class' => implode(
231 ' ',
232 array_filter(
233 [
234 'usk-button',
235 'product_type_' . $product->get_type(),
236 $product->is_purchasable() && $product->is_in_stock() ? 'add_to_cart_button' : '',
237 $product->supports('ajax_add_to_cart') && $product->is_purchasable() && $product->is_in_stock() ? 'ajax_add_to_cart' : '',
238 ]
239 )
240 ),
241 'attributes' => [
242 'data-product_id' => $product->get_id(),
243 'data-product_sku' => $product->get_sku(),
244 'aria-label' => $product->add_to_cart_description(),
245 'rel' => 'nofollow',
246 ],
247 ];
248 $args = apply_filters('woocommerce_loop_add_to_cart_args', wp_parse_args($defaults), $product);
249 if (isset($args['attributes']['aria-label'])) {
250 $args['attributes']['aria-label'] = wp_strip_all_tags($args['attributes']['aria-label']);
251 }
252
253 if (isset($settings['show_cart']) ? $settings['show_cart'] : true) {
254 echo wp_kses_post(apply_filters(
255 'woocommerce_loop_add_to_cart_link', // WPCS: XSS ok.
256 sprintf(
257 '<a href="%s" data-quantity="%s" class="%s" %s>%s <i class="button-icon usk-icon-arrow-right-8"></i></a>',
258 esc_url($product->add_to_cart_url()),
259 esc_attr(isset($args['quantity']) ? $args['quantity'] : 1),
260 esc_attr(isset($args['class']) ? $args['class'] : 'button'),
261 isset($args['attributes']) ? wc_implode_html_attributes($args['attributes']) : '',
262 esc_html($product->add_to_cart_text())
263 ),
264 $product,
265 $args
266 ));
267 }
268 };
269 }
270
271 /**
272 * Get variation ID from product attributes
273 * Uses WooCommerce data store for reliable variation finding
274 */
275 private function get_variation_id_from_attributes($product, $attributes) {
276 if (!$product || !$product->is_type('variable')) {
277 return null;
278 }
279
280 // Format attributes for WooCommerce
281 $formatted_attributes = [];
282 foreach ($attributes as $attribute_name => $attribute_value) {
283 $formatted_attributes['attribute_' . $attribute_name] = $attribute_value;
284 }
285
286 // Use WooCommerce's data store to find matching variation
287 $data_store = \WC_Data_Store::load('product');
288 $variation_id = $data_store->find_matching_product_variation($product, $formatted_attributes);
289
290 if ($variation_id) {
291 return $variation_id;
292 }
293
294 return null;
295 }
296
297 /**
298 * Render product image with hover effect and action buttons
299 */
300 public function render_product_image($product) {
301 if (!$product) {
302 return;
303 }
304
305 $tooltip_position = 'left';
306 $settings = $this->target_widget_settings;
307 $image_size = isset($settings['image_size']) ? $settings['image_size'] : 'full';
308
309 // Get main product image
310 $product_image = wp_get_attachment_image_url(get_post_thumbnail_id(), $image_size);
311
312 // if product image is empty, use placeholder
313 if (empty($product_image)) {
314 $product_image = wc_placeholder_img_src('full');
315 }
316 // Get gallery image for hover effect
317 $gallery_image_link = $product_image; // Default to main image
318 $gallery_thumbs = $product->get_gallery_image_ids();
319
320 if ($gallery_thumbs && !empty($gallery_thumbs[0])) {
321 $gallery_image_link = wp_get_attachment_image_url($gallery_thumbs[0], $image_size);
322 }
323 ?>
324 <div class="usk-image">
325 <a href="<?php echo esc_url(get_permalink()); ?>">
326 <img
327 class="img"
328 src="<?php echo esc_url($product_image); ?>"
329 alt="<?php echo esc_attr(get_the_title()); ?>">
330 </a>
331 <?php $this->render_add_to_cart_button($product, $settings); ?>
332
333 <div class="usk-shoping">
334 <?php $this->register_global_template_add_to_wishlist($tooltip_position, $settings); ?>
335 <?php $this->register_global_template_add_to_compare($tooltip_position, $settings); ?>
336 <?php $this->register_global_template_quick_view($product->get_id(), $tooltip_position, $settings); ?>
337 </div>
338 <div class="usk-badge-label-wrapper">
339 <div class="usk-badge-label-content usk-flex usk-flex-column">
340 <?php $this->register_global_template_badge_label($settings); ?>
341 </div>
342
343 <!-- display product variation -->
344 </div>
345 </div>
346 <?php
347 }
348
349 /**
350 * Print price HTML with allowed tags
351 */
352 public function print_price_output($output) {
353 $allowed_tags = [
354 'del' => ['aria-hidden' => []],
355 'span' => ['class' => []],
356 'bdi' => [],
357 'ins' => [],
358 ];
359
360 if (isset($output)) {
361 echo wp_kses($output, $allowed_tags);
362 }
363 }
364
365 /**
366 * Check if swatches support is available
367 */
368 private function has_swatches_support() {
369 return class_exists('UltimateStoreKitPro\\VariationSwatches\\Swatches');
370 }
371
372 /**
373 * Render product variation options (colors, sizes)
374 * Displays variation swatches on product grid items
375 */
376 public function render_product_variation($product, $settings) {
377 if (!$product || !$product->is_type('variable')) {
378 return;
379 }
380
381 $variations = $product->get_available_variations();
382 if (empty($variations)) {
383 return;
384 }
385
386 // Check if sequential mode is enabled
387 $sequential = \apply_filters('usk_sequential_variations', isset($settings['show_variation_sequential']) && $settings['show_variation_sequential'] === 'yes');
388
389 // If Pro version with swatches is active, use that functionality
390 if ($this->has_swatches_support() && function_exists('apply_filters')) {
391 $this->render_swatches_variation($product, $variations, $sequential);
392 return;
393 }
394
395 // Otherwise use the simple variation buttons
396 $this->render_simple_variations($product, $variations, $sequential);
397 }
398
399 /**
400 * Render simple variation buttons for the free version
401 */
402 private function render_simple_variations($product, $variations, $sequential = false) {
403 $product_id = $product->get_id();
404 $attributes = $product->get_variation_attributes();
405
406 if (empty($attributes)) {
407 return;
408 }
409
410 // Add sequential data attribute if enabled
411 if ($sequential) {
412 echo '<div class="usk-variations-container" data-product-id="' . esc_attr($product_id) . '" data-variations-reset="true" data-sequential="true">';
413 } else {
414 echo '<div class="usk-variations-container" data-product-id="' . esc_attr($product_id) . '" data-variations-reset="true">';
415 }
416
417 foreach ($attributes as $attribute_name => $options) {
418 if (empty($options)) {
419 continue;
420 }
421
422 // Get attribute label
423 $attribute_label = wc_attribute_label($attribute_name);
424 $attribute_slug = sanitize_title($attribute_name);
425
426 echo '<div class="usk-variation-group">';
427 echo '<div class="usk-variation-options">';
428
429 foreach ($options as $option) {
430 $classes = 'usk-variation-button';
431 $option_name = apply_filters('woocommerce_variation_option_name', $option);
432
433 // Check if this is a color attribute
434 $is_color = (strpos(strtolower($attribute_slug), 'color') !== false ||
435 strpos(strtolower($attribute_label), 'color') !== false);
436
437 if ($is_color) {
438 // For color attributes, use background color and minimal text
439 // Translators: %s is the name of the color option (e.g., "Red", "Blue").
440 $aria_label = sprintf(__('Select %s', 'ultimate-store-kit'), esc_attr($option_name));
441 echo '<button type="button" class="' . esc_attr($classes . ' usk-color-button') . '"
442 data-attribute="' . esc_attr($attribute_slug) . '"
443 data-value="' . esc_attr($option) . '"
444 style="background-color: ' . esc_attr($option) . '"
445 aria-label="' . esc_attr($aria_label) . '"><span class="usk-tooltip-text">' . esc_html($option_name) . '</span>
446 </button>';
447 } else {
448 // For non-color attributes, display as regular text buttons
449 // Translators: %s is the name of the variation option (e.g., "Large", "Cotton").
450 $aria_label = sprintf(__('Select %s', 'ultimate-store-kit'), esc_attr($option_name));
451 echo '<button type="button" class="' . esc_attr($classes) . '"
452 data-attribute="' . esc_attr($attribute_slug) . '"
453 data-value="' . esc_attr($option) . '"
454 aria-label="' . esc_attr($aria_label) . '">' . esc_html($option_name) . '<span class="usk-tooltip-text">' . esc_html($option_name) . '</span>
455 </button>';
456 }
457 }
458
459 echo '</div>'; // Close .usk-variation-options
460 echo '</div>'; // Close .usk-variation-group
461 }
462
463 echo '</div>'; // Close .usk-variations-container
464 }
465
466 /**
467 * Render variation swatches using the Pro version's swatches functionality
468 */
469 private function render_swatches_variation($product, $variations, $sequential = false) {
470 $product_id = $product->get_id();
471 $attributes = $product->get_variation_attributes();
472
473 if (empty($attributes)) {
474 return;
475 }
476
477 // Add sequential data attribute if enabled
478 if ($sequential) {
479 echo '<div class="usk-variations-container usk-pro-swatches" data-product-id="' . esc_attr($product_id) . '" data-variations-reset="true" data-sequential="true">';
480 } else {
481 echo '<div class="usk-variations-container usk-pro-swatches" data-product-id="' . esc_attr($product_id) . '" data-variations-reset="true">';
482 }
483
484 // Loop through each product attribute
485 foreach ($attributes as $attribute_name => $options) {
486 if (empty($options)) {
487 continue;
488 }
489
490 echo '<div class="usk-variation-group">';
491
492 // Build the args for the swatches
493 $args = array(
494 'options' => $options,
495 'product' => $product,
496 'attribute' => $attribute_name,
497 'name' => 'attribute_' . sanitize_title($attribute_name),
498 // phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Preselects a variation from the shop URL; wc_clean() is WooCommerce's own sanitizer.
499 'selected' => isset($_REQUEST['attribute_' . sanitize_title($attribute_name)])
500 ? wc_clean(wp_unslash($_REQUEST['attribute_' . sanitize_title($attribute_name)]))
501 : $product->get_variation_default_attribute($attribute_name)
502 // phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
503 );
504
505 // Create a placeholder for the dropdown - this will be replaced with swatches
506 $dropdown_html = '<select id="' . esc_attr($args['name']) . '" class="' . esc_attr($args['name']) . '" name="' . esc_attr($args['name']) . '" data-attribute_name="' . esc_attr($args['name']) . '" data-show_option_none="yes" style="display:none;">';
507 $dropdown_html .= '<option value="">' . esc_html__('Choose an option', 'ultimate-store-kit') . '</option>';
508
509 if (!empty($options)) {
510 foreach ($options as $option) {
511 $dropdown_html .= '<option value="' . esc_attr($option) . '" ' . selected($args['selected'], $option, false) . '>' . esc_html(apply_filters('woocommerce_variation_option_name', $option, null, $attribute_name, $product)) . '</option>';
512 }
513 }
514
515 $dropdown_html .= '</select>';
516
517 // Apply the filter to transform the dropdown to swatches
518 if (class_exists('UltimateStoreKitPro\\VariationSwatches\\Swatches')) {
519 $swatches = \UltimateStoreKitPro\VariationSwatches\Swatches::instance();
520 $swatches_html = $swatches->swatches_html($dropdown_html, $args);
521 echo $swatches_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Markup built from the already-escaped $dropdown_html.
522 } else {
523 echo $dropdown_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built above entirely from esc_attr()/esc_html()ed values.
524 }
525
526 echo '</div>'; // Close .usk-variation-group
527 }
528
529 echo '</div>'; // Close .usk-variations-container
530 }
531 }
532