PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.87
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.87
51.1.87 51.1.86 51.1.84 51.1.85 51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 All 41 releases
king-addons / includes / widgets / Quick_Product_Grid / Quick_Product_Grid.php

Quick_Product_Grid.php in King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder 51.1.87, at includes/widgets/Quick_Product_Grid/Quick_Product_Grid.php

1,669 lines 53.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Quick Product Grid Widget (Free).
4 *
5 * @package King_Addons
6 */
7
8 namespace King_Addons;
9
10 use Elementor\Controls_Manager;
11 use Elementor\Group_Control_Border;
12 use Elementor\Group_Control_Box_Shadow;
13 use Elementor\Group_Control_Image_Size;
14 use Elementor\Group_Control_Typography;
15 use Elementor\Widget_Base;
16 use WP_Query;
17
18 if (!defined('ABSPATH')) {
19 exit;
20 }
21
22 /**
23 * Renders the Quick Product Grid widget.
24 */
25 class Quick_Product_Grid extends Widget_Base
26 {
27 /**
28 * External query arguments injected from faceted filters.
29 *
30 * @var array<string, mixed>
31 */
32 protected array $external_query_args = [];
33
34 /**
35 * Wrapper render attribute handle shared with faceted filters feature.
36 */
37 private const FILTER_WRAPPER_HANDLE = 'ka-filters-wrapper';
38
39 /**
40 * Widget slug.
41 *
42 * @return string
43 */
44 public function get_name(): string
45 {
46 return 'king-addons-quick-product-grid';
47 }
48
49 /**
50 * Widget title.
51 *
52 * @return string
53 */
54 public function get_title(): string
55 {
56 return esc_html__('Quick Product Grid', 'king-addons');
57 }
58
59 /**
60 * Widget icon.
61 *
62 * @return string
63 */
64 public function get_icon(): string
65 {
66 return 'king-addons-icon king-addons-quick-product-grid';
67 }
68
69 /**
70 * Style dependencies.
71 *
72 * @return array<int, string>
73 */
74 public function get_style_depends(): array
75 {
76 return [
77 KING_ADDONS_ASSETS_UNIQUE_KEY . '-quick-product-grid-style',
78 ];
79 }
80
81 /**
82 * Script dependencies.
83 *
84 * @return array<int, string>
85 */
86 public function get_script_depends(): array
87 {
88 $deps = [
89 KING_ADDONS_ASSETS_UNIQUE_KEY . '-quick-product-grid-script',
90 ];
91
92 if (class_exists('\WooCommerce') && function_exists('wp_script_is') && wp_script_is('wc-add-to-cart', 'registered')) {
93 $deps[] = 'wc-add-to-cart';
94 }
95
96 return $deps;
97 }
98
99 /**
100 * Categories.
101 *
102 * @return array<int, string>
103 */
104 public function get_categories(): array
105 {
106 return ['king-addons-woo'];
107 }
108
109 /**
110 * Keywords.
111 *
112 * @return array<int, string>
113 */
114 public function get_keywords(): array
115 {
116 return ['woocommerce', 'product', 'quick', 'grid', 'shop'];
117 }
118
119 public function get_custom_help_url()
120 {
121 return 'mailto:[email protected]?subject=Bug Report - King Addons&body=Please describe the issue';
122 }
123
124 /**
125 * Register controls.
126 *
127 * @return void
128 */
129 public function register_controls(): void
130 {
131 $this->register_query_controls();
132 $this->register_layout_controls();
133 $this->register_display_controls();
134 $this->register_interaction_controls();
135 $this->register_style_card_controls();
136 $this->register_style_text_controls();
137 $this->register_style_rating_controls();
138 $this->register_style_badge_controls();
139 $this->register_style_button_controls();
140 $this->register_style_view_cart_controls();
141 $this->register_pro_notice_controls();
142 }
143
144 /**
145 * Interaction controls.
146 *
147 * @return void
148 */
149 protected function register_interaction_controls(): void
150 {
151 $this->start_controls_section(
152 'kng_interaction_section',
153 [
154 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Animations', 'king-addons'),
155 'tab' => Controls_Manager::TAB_CONTENT,
156 ]
157 );
158
159 $this->add_control(
160 'kng_hover_animation',
161 [
162 'label' => esc_html__('Hover Animation', 'king-addons'),
163 'type' => Controls_Manager::SELECT,
164 'default' => 'hover-small-lift',
165 'options' => [
166 'none' => esc_html__('None', 'king-addons'),
167 'hover-small-lift' => esc_html__('Small Lift', 'king-addons'),
168 'hover-lift' => esc_html__('Lift', 'king-addons'),
169 'hover-zoom' => esc_html__('Image Zoom', 'king-addons'),
170 'hover-tilt' => esc_html__('Tilt', 'king-addons'),
171 'hover-float' => esc_html__('Float', 'king-addons'),
172 'hover-scale-fade' => esc_html__('Scale & Fade', 'king-addons'),
173 ],
174 ]
175 );
176
177 $this->end_controls_section();
178 }
179
180 /**
181 * Render widget output.
182 *
183 * @return void
184 */
185 public function render(): void
186 {
187 if (!class_exists('\WooCommerce')) {
188 return;
189 }
190
191 $settings = $this->get_settings_for_display();
192 $query = $this->build_query($settings);
193 $wrapper_classes = $this->get_wrapper_classes($settings);
194 $wrapper_style = $this->get_wrapper_style($settings);
195 $wrapper_handle = self::FILTER_WRAPPER_HANDLE;
196
197 $this->add_render_attribute($wrapper_handle, 'class', $wrapper_classes);
198
199 if ('' !== $wrapper_style) {
200 $this->add_render_attribute($wrapper_handle, 'style', $wrapper_style);
201 }
202
203 echo '<div ' . $this->get_render_attribute_string($wrapper_handle) . '>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
204 if (!$query->have_posts()) {
205 echo '<p class="king-addons-quick-product-grid__empty">' . esc_html__('No products found.', 'king-addons') . '</p>';
206 echo '</div>';
207 wp_reset_postdata();
208 return;
209 }
210 ?>
211 <div class="king-addons-quick-product-grid__grid">
212 <?php
213 while ($query->have_posts()) :
214 $query->the_post();
215 $product = wc_get_product(get_the_ID());
216 if ($product) {
217 $this->render_card($settings, $product);
218 }
219 endwhile;
220 ?>
221 </div>
222 </div>
223 <?php
224
225 wp_reset_postdata();
226 }
227
228 /**
229 * Query controls.
230 *
231 * @return void
232 */
233 protected function register_query_controls(): void
234 {
235 $this->start_controls_section(
236 'kng_query_section',
237 [
238 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Query', 'king-addons'),
239 'tab' => Controls_Manager::TAB_CONTENT,
240 ]
241 );
242
243 $this->add_control(
244 'kng_products_per_page',
245 [
246 'label' => esc_html__('Products Number', 'king-addons'),
247 'type' => Controls_Manager::NUMBER,
248 'min' => 1,
249 'max' => 12,
250 'step' => 1,
251 'default' => 6,
252 'description' => esc_html__('Free version limited to 12 products.', 'king-addons'),
253 ]
254 );
255
256 $this->add_control(
257 'kng_orderby',
258 [
259 'label' => esc_html__('Order By', 'king-addons'),
260 'type' => Controls_Manager::SELECT,
261 'default' => 'date',
262 'options' => [
263 'date' => esc_html__('Date', 'king-addons'),
264 'title' => esc_html__('Title', 'king-addons'),
265 'price' => esc_html__('Price', 'king-addons'),
266 'popularity' => esc_html__('Popularity', 'king-addons'),
267 'rating' => esc_html__('Rating', 'king-addons'),
268 'rand' => esc_html__('Random', 'king-addons'),
269 ],
270 ]
271 );
272
273 $this->add_control(
274 'kng_order',
275 [
276 'label' => esc_html__('Order', 'king-addons'),
277 'type' => Controls_Manager::SELECT,
278 'default' => 'DESC',
279 'options' => [
280 'DESC' => esc_html__('DESC', 'king-addons'),
281 'ASC' => esc_html__('ASC', 'king-addons'),
282 ],
283 ]
284 );
285
286 $this->add_control(
287 'kng_categories',
288 [
289 'label' => sprintf(__('Categories %s', 'king-addons'), '<i class="eicon-pro-icon"></i>'),
290 'type' => Controls_Manager::TEXT,
291 'description' => esc_html__('Comma-separated slugs.', 'king-addons'),
292 'classes' => 'king-addons-pro-control no-distance',
293 ]
294 );
295
296 $this->add_control(
297 'kng_include_products',
298 [
299 'label' => esc_html__('Include Products', 'king-addons'),
300 'type' => Controls_Manager::TEXT,
301 'description' => esc_html__('Comma-separated product IDs.', 'king-addons'),
302 ]
303 );
304
305 $this->add_control(
306 'kng_exclude_products',
307 [
308 'label' => esc_html__('Exclude Products', 'king-addons'),
309 'type' => Controls_Manager::TEXT,
310 'description' => esc_html__('Comma-separated product IDs.', 'king-addons'),
311 ]
312 );
313
314 $this->add_control(
315 'kng_on_sale',
316 [
317 'label' => esc_html__('Show Only On Sale', 'king-addons'),
318 'type' => Controls_Manager::SWITCHER,
319 'return_value' => 'yes',
320 'default' => '',
321 ]
322 );
323
324 $this->add_control(
325 'kng_featured_only',
326 [
327 'label' => esc_html__('Show Only Featured', 'king-addons'),
328 'type' => Controls_Manager::SWITCHER,
329 'return_value' => 'yes',
330 'default' => '',
331 ]
332 );
333
334 $this->add_control(
335 'kng_in_stock',
336 [
337 'label' => esc_html__('Only In Stock', 'king-addons'),
338 'type' => Controls_Manager::SWITCHER,
339 'return_value' => 'yes',
340 'default' => '',
341 ]
342 );
343
344 $this->end_controls_section();
345 }
346
347 /**
348 * Layout controls.
349 *
350 * @return void
351 */
352 protected function register_layout_controls(): void
353 {
354 $this->start_controls_section(
355 'kng_layout_section',
356 [
357 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Layout', 'king-addons'),
358 'tab' => Controls_Manager::TAB_CONTENT,
359 ]
360 );
361
362 $this->add_responsive_control(
363 'kng_columns',
364 [
365 'label' => esc_html__('Columns', 'king-addons'),
366 'type' => Controls_Manager::NUMBER,
367 'min' => 1,
368 'max' => 6,
369 'step' => 1,
370 'default' => 3,
371 'tablet_default' => 2,
372 'mobile_default' => 1,
373 'selectors' => [
374 '{{WRAPPER}} .king-addons-quick-product-grid__grid' => 'grid-template-columns: repeat({{VALUE}}, minmax(0, 1fr));',
375 ],
376 ]
377 );
378
379 $this->add_responsive_control(
380 'kng_grid_gap',
381 [
382 'label' => esc_html__('Gap', 'king-addons'),
383 'type' => Controls_Manager::SLIDER,
384 'size_units' => ['px'],
385 'range' => [
386 'px' => [
387 'min' => 0,
388 'max' => 80,
389 ],
390 ],
391 'default' => [
392 'size' => 20,
393 'unit' => 'px',
394 ],
395 'selectors' => [
396 '{{WRAPPER}} .king-addons-quick-product-grid__grid' => 'gap: {{SIZE}}{{UNIT}};',
397 ],
398 ]
399 );
400
401 $this->end_controls_section();
402 }
403
404 /**
405 * Display controls.
406 *
407 * @return void
408 */
409 protected function register_display_controls(): void
410 {
411 $this->start_controls_section(
412 'kng_display_section',
413 [
414 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Display', 'king-addons'),
415 'tab' => Controls_Manager::TAB_CONTENT,
416 ]
417 );
418
419 $this->add_control(
420 'kng_show_rating',
421 [
422 'label' => esc_html__('Show Rating', 'king-addons'),
423 'type' => Controls_Manager::SWITCHER,
424 'return_value' => 'yes',
425 'default' => 'yes',
426 ]
427 );
428
429 $this->add_control(
430 'kng_show_price',
431 [
432 'label' => esc_html__('Show Price', 'king-addons'),
433 'type' => Controls_Manager::SWITCHER,
434 'return_value' => 'yes',
435 'default' => 'yes',
436 ]
437 );
438
439 $this->add_control(
440 'kng_show_excerpt',
441 [
442 'label' => esc_html__('Show Short Description', 'king-addons'),
443 'type' => Controls_Manager::SWITCHER,
444 'return_value' => 'yes',
445 'default' => 'yes',
446 ]
447 );
448
449 $this->add_control(
450 'kng_show_add_to_cart',
451 [
452 'label' => esc_html__('Show Add to Cart', 'king-addons'),
453 'type' => Controls_Manager::SWITCHER,
454 'return_value' => 'yes',
455 'default' => 'yes',
456 ]
457 );
458
459 $this->add_control(
460 'kng_show_badge',
461 [
462 'label' => esc_html__('Show Sale Badge', 'king-addons'),
463 'type' => Controls_Manager::SWITCHER,
464 'return_value' => 'yes',
465 'default' => 'yes',
466 ]
467 );
468
469 $this->add_control(
470 'kng_badge_text',
471 [
472 'label' => esc_html__('Sale Badge Text', 'king-addons'),
473 'type' => Controls_Manager::TEXT,
474 'default' => esc_html__('Sale', 'king-addons'),
475 'condition' => [
476 'kng_show_badge' => 'yes',
477 ],
478 ]
479 );
480
481 $this->add_control(
482 'kng_excerpt_length',
483 [
484 'label' => esc_html__('Excerpt Length (words)', 'king-addons'),
485 'type' => Controls_Manager::NUMBER,
486 'min' => 5,
487 'max' => 60,
488 'step' => 1,
489 'default' => 16,
490 'condition' => [
491 'kng_show_excerpt' => 'yes',
492 ],
493 ]
494 );
495
496 $this->add_group_control(
497 Group_Control_Image_Size::get_type(),
498 [
499 'name' => 'kng_image_size',
500 'default' => 'medium',
501 ]
502 );
503
504 $this->end_controls_section();
505 }
506
507 /**
508 * Card style controls.
509 *
510 * @return void
511 */
512 protected function register_style_card_controls(): void
513 {
514 $this->start_controls_section(
515 'kng_style_card_section',
516 [
517 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Card', 'king-addons'),
518 'tab' => Controls_Manager::TAB_STYLE,
519 ]
520 );
521
522 $this->add_control(
523 'kng_card_background',
524 [
525 'label' => esc_html__('Background', 'king-addons'),
526 'type' => Controls_Manager::COLOR,
527 'selectors' => [
528 '{{WRAPPER}} .king-addons-quick-product-grid__card' => 'background-color: {{VALUE}};',
529 ],
530 ]
531 );
532
533 $this->add_group_control(
534 Group_Control_Border::get_type(),
535 [
536 'name' => 'kng_card_border',
537 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__card',
538 ]
539 );
540
541 $this->add_control(
542 'kng_card_radius',
543 [
544 'label' => esc_html__('Border Radius', 'king-addons'),
545 'type' => Controls_Manager::SLIDER,
546 'size_units' => ['px', '%'],
547 'range' => [
548 'px' => ['min' => 0, 'max' => 50],
549 '%' => ['min' => 0, 'max' => 100],
550 ],
551 'selectors' => [
552 '{{WRAPPER}} .king-addons-quick-product-grid__card' => 'border-radius: {{SIZE}}{{UNIT}};',
553 ],
554 ]
555 );
556
557 $this->add_group_control(
558 Group_Control_Box_Shadow::get_type(),
559 [
560 'name' => 'kng_card_shadow',
561 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__card',
562 ]
563 );
564
565 $this->add_control(
566 'kng_card_shadow_disable',
567 [
568 'label' => esc_html__('Disable Box Shadow', 'king-addons'),
569 'type' => Controls_Manager::SWITCHER,
570 'label_on' => esc_html__('Yes', 'king-addons'),
571 'label_off' => esc_html__('No', 'king-addons'),
572 'return_value' => 'yes',
573 'selectors' => [
574 '{{WRAPPER}} .king-addons-quick-product-grid__card' => 'box-shadow: none !important;',
575 ],
576 ]
577 );
578
579 $this->add_control(
580 'kng_card_shadow_hover_disable',
581 [
582 'label' => esc_html__('Disable Box Shadow on Hover', 'king-addons'),
583 'type' => Controls_Manager::SWITCHER,
584 'label_on' => esc_html__('Yes', 'king-addons'),
585 'label_off' => esc_html__('No', 'king-addons'),
586 'return_value' => 'yes',
587 'selectors' => [
588 '{{WRAPPER}} .king-addons-quick-product-grid__card:hover' => 'box-shadow: none !important;',
589 ],
590 ]
591 );
592
593 $this->add_control(
594 'kng_card_padding',
595 [
596 'label' => esc_html__('Padding', 'king-addons'),
597 'type' => Controls_Manager::DIMENSIONS,
598 'size_units' => ['px', '%', 'em'],
599 'selectors' => [
600 '{{WRAPPER}} .king-addons-quick-product-grid__card' => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
601 ],
602 ]
603 );
604
605 $this->end_controls_section();
606 }
607
608 /**
609 * Text styles.
610 *
611 * @return void
612 */
613 protected function register_style_text_controls(): void
614 {
615 $this->start_controls_section(
616 'kng_style_text_section',
617 [
618 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Text', 'king-addons'),
619 'tab' => Controls_Manager::TAB_STYLE,
620 ]
621 );
622
623 $this->add_group_control(
624 Group_Control_Typography::get_type(),
625 [
626 'name' => 'kng_title_typography',
627 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__title',
628 ]
629 );
630
631 $this->add_control(
632 'kng_title_color',
633 [
634 'label' => esc_html__('Title Color', 'king-addons'),
635 'type' => Controls_Manager::COLOR,
636 'selectors' => [
637 '{{WRAPPER}} .king-addons-quick-product-grid__title a' => 'color: {{VALUE}};',
638 ],
639 ]
640 );
641
642 $this->add_group_control(
643 Group_Control_Typography::get_type(),
644 [
645 'name' => 'kng_price_typography',
646 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__price',
647 ]
648 );
649
650 $this->add_control(
651 'kng_price_color',
652 [
653 'label' => esc_html__('Price Color', 'king-addons'),
654 'type' => Controls_Manager::COLOR,
655 'selectors' => [
656 '{{WRAPPER}} .king-addons-quick-product-grid__price' => 'color: {{VALUE}};',
657 ],
658 ]
659 );
660
661 $this->add_group_control(
662 Group_Control_Typography::get_type(),
663 [
664 'name' => 'kng_excerpt_typography',
665 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__excerpt',
666 ]
667 );
668
669 $this->add_control(
670 'kng_excerpt_color',
671 [
672 'label' => esc_html__('Excerpt Color', 'king-addons'),
673 'type' => Controls_Manager::COLOR,
674 'selectors' => [
675 '{{WRAPPER}} .king-addons-quick-product-grid__excerpt' => 'color: {{VALUE}};',
676 ],
677 ]
678 );
679
680 $this->end_controls_section();
681 }
682
683 /**
684 * Rating styles.
685 *
686 * @return void
687 */
688 protected function register_style_rating_controls(): void
689 {
690 $this->start_controls_section(
691 'kng_style_rating_section',
692 [
693 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Rating', 'king-addons'),
694 'tab' => Controls_Manager::TAB_STYLE,
695 'condition' => [
696 'kng_show_rating' => 'yes',
697 ],
698 ]
699 );
700
701 $this->add_control(
702 'kng_rating_color_empty',
703 [
704 'label' => esc_html__('Empty Color', 'king-addons'),
705 'type' => Controls_Manager::COLOR,
706 'selectors' => [
707 '{{WRAPPER}} .king-addons-quick-product-grid__rating .star-rating' => 'color: {{VALUE}};',
708 ],
709 ]
710 );
711
712 $this->add_control(
713 'kng_rating_color_filled',
714 [
715 'label' => esc_html__('Filled Color', 'king-addons'),
716 'type' => Controls_Manager::COLOR,
717 'selectors' => [
718 '{{WRAPPER}} .king-addons-quick-product-grid__rating .star-rating span' => 'color: {{VALUE}};',
719 ],
720 ]
721 );
722
723 $this->add_control(
724 'kng_rating_size',
725 [
726 'label' => esc_html__('Size', 'king-addons'),
727 'type' => Controls_Manager::SLIDER,
728 'size_units' => ['px'],
729 'range' => [
730 'px' => ['min' => 10, 'max' => 32],
731 ],
732 'selectors' => [
733 '{{WRAPPER}} .king-addons-quick-product-grid__rating .star-rating' => 'font-size: {{SIZE}}{{UNIT}};',
734 ],
735 ]
736 );
737
738 $this->end_controls_section();
739 }
740
741 /**
742 * Sale badge styles.
743 *
744 * @return void
745 */
746 protected function register_style_badge_controls(): void
747 {
748 $this->start_controls_section(
749 'kng_style_badge_section',
750 [
751 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Sale Badge', 'king-addons'),
752 'tab' => Controls_Manager::TAB_STYLE,
753 'condition' => [
754 'kng_show_badge' => 'yes',
755 ],
756 ]
757 );
758
759 $this->add_group_control(
760 Group_Control_Typography::get_type(),
761 [
762 'name' => 'kng_badge_typography',
763 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__badge',
764 ]
765 );
766
767 $this->add_control(
768 'kng_badge_color',
769 [
770 'label' => esc_html__('Text Color', 'king-addons'),
771 'type' => Controls_Manager::COLOR,
772 'selectors' => [
773 '{{WRAPPER}} .king-addons-quick-product-grid__badge' => 'color: {{VALUE}};',
774 ],
775 ]
776 );
777
778 $this->add_control(
779 'kng_badge_background',
780 [
781 'label' => esc_html__('Background', 'king-addons'),
782 'type' => Controls_Manager::COLOR,
783 'selectors' => [
784 '{{WRAPPER}} .king-addons-quick-product-grid__badge' => 'background-color: {{VALUE}};',
785 ],
786 ]
787 );
788
789 $this->add_group_control(
790 Group_Control_Border::get_type(),
791 [
792 'name' => 'kng_badge_border',
793 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__badge',
794 ]
795 );
796
797 $this->add_group_control(
798 Group_Control_Box_Shadow::get_type(),
799 [
800 'name' => 'kng_badge_shadow',
801 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__badge',
802 ]
803 );
804
805 $this->add_control(
806 'kng_badge_radius',
807 [
808 'label' => esc_html__('Border Radius', 'king-addons'),
809 'type' => Controls_Manager::SLIDER,
810 'size_units' => ['px', '%'],
811 'range' => [
812 'px' => ['min' => 0, 'max' => 50],
813 '%' => ['min' => 0, 'max' => 100],
814 ],
815 'selectors' => [
816 '{{WRAPPER}} .king-addons-quick-product-grid__badge' => 'border-radius: {{SIZE}}{{UNIT}};',
817 ],
818 ]
819 );
820
821 $this->add_responsive_control(
822 'kng_badge_padding',
823 [
824 'label' => esc_html__('Padding', 'king-addons'),
825 'type' => Controls_Manager::DIMENSIONS,
826 'size_units' => ['px', 'em'],
827 'selectors' => [
828 '{{WRAPPER}} .king-addons-quick-product-grid__badge' => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
829 ],
830 ]
831 );
832
833 $this->add_responsive_control(
834 'kng_badge_offset_top',
835 [
836 'label' => esc_html__('Offset Top', 'king-addons'),
837 'type' => Controls_Manager::SLIDER,
838 'size_units' => ['px'],
839 'range' => [
840 'px' => ['min' => 0, 'max' => 80],
841 ],
842 'selectors' => [
843 '{{WRAPPER}} .king-addons-quick-product-grid__badge' => 'top: {{SIZE}}{{UNIT}};',
844 ],
845 ]
846 );
847
848 $this->add_responsive_control(
849 'kng_badge_offset_left',
850 [
851 'label' => esc_html__('Offset Left', 'king-addons'),
852 'type' => Controls_Manager::SLIDER,
853 'size_units' => ['px'],
854 'range' => [
855 'px' => ['min' => 0, 'max' => 80],
856 ],
857 'selectors' => [
858 '{{WRAPPER}} .king-addons-quick-product-grid__badge' => 'left: {{SIZE}}{{UNIT}};',
859 ],
860 ]
861 );
862
863 $this->end_controls_section();
864 }
865
866 /**
867 * Button styles.
868 *
869 * @return void
870 */
871 protected function register_style_button_controls(): void
872 {
873 $this->start_controls_section(
874 'kng_style_button_section',
875 [
876 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Add to Cart', 'king-addons'),
877 'tab' => Controls_Manager::TAB_STYLE,
878 ]
879 );
880
881 $this->add_responsive_control(
882 'kng_cta_alignment',
883 [
884 'label' => esc_html__('Alignment', 'king-addons'),
885 'type' => Controls_Manager::CHOOSE,
886 'options' => [
887 'flex-start' => [
888 'title' => esc_html__('Start', 'king-addons'),
889 'icon' => 'eicon-h-align-left',
890 ],
891 'center' => [
892 'title' => esc_html__('Center', 'king-addons'),
893 'icon' => 'eicon-h-align-center',
894 ],
895 'flex-end' => [
896 'title' => esc_html__('End', 'king-addons'),
897 'icon' => 'eicon-h-align-right',
898 ],
899 ],
900 'toggle' => false,
901 'default' => 'flex-start',
902 'selectors' => [
903 '{{WRAPPER}} .king-addons-quick-product-grid__cta' => 'justify-content: {{VALUE}};',
904 ],
905 ]
906 );
907
908 $this->add_control(
909 'kng_button_full_width',
910 [
911 'label' => esc_html__('Full Width Button', 'king-addons'),
912 'type' => Controls_Manager::SWITCHER,
913 'return_value' => 'yes',
914 'default' => '',
915 'selectors' => [
916 '{{WRAPPER}} .king-addons-quick-product-grid__cta' => 'width: 100%;',
917 '{{WRAPPER}} .king-addons-quick-product-grid__cta .button' => 'width: 100%; justify-content: center;',
918 ],
919 ]
920 );
921
922 $this->add_group_control(
923 Group_Control_Typography::get_type(),
924 [
925 'name' => 'kng_button_typography',
926 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__cta .button',
927 ]
928 );
929
930 $this->add_responsive_control(
931 'kng_button_padding',
932 [
933 'label' => esc_html__('Padding', 'king-addons'),
934 'type' => Controls_Manager::DIMENSIONS,
935 'size_units' => ['px', 'em'],
936 'selectors' => [
937 '{{WRAPPER}} .king-addons-quick-product-grid__cta .button' => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
938 ],
939 ]
940 );
941
942 $this->add_responsive_control(
943 'kng_button_indicator_size',
944 [
945 'label' => esc_html__('Indicator Size', 'king-addons'),
946 'type' => Controls_Manager::SLIDER,
947 'size_units' => ['px'],
948 'range' => [
949 'px' => ['min' => 8, 'max' => 40],
950 ],
951 'selectors' => [
952 '{{WRAPPER}} .king-addons-quick-product-grid__cta .king-addons-quick-product-grid__add-to-cart' => '--king-addons-atc-indicator-size: {{SIZE}}{{UNIT}};',
953 ],
954 ]
955 );
956
957 $this->start_controls_tabs('kng_button_style_tabs');
958
959 $this->start_controls_tab(
960 'kng_button_style_tab_normal',
961 [
962 'label' => esc_html__('Normal', 'king-addons'),
963 ]
964 );
965
966 $this->add_control(
967 'kng_button_color',
968 [
969 'label' => esc_html__('Text Color', 'king-addons'),
970 'type' => Controls_Manager::COLOR,
971 'selectors' => [
972 '{{WRAPPER}} .king-addons-quick-product-grid__cta .button' => 'color: {{VALUE}};',
973 ],
974 ]
975 );
976
977 $this->add_control(
978 'kng_button_bg',
979 [
980 'label' => esc_html__('Background', 'king-addons'),
981 'type' => Controls_Manager::COLOR,
982 'selectors' => [
983 '{{WRAPPER}} .king-addons-quick-product-grid__cta .button' => 'background-color: {{VALUE}};',
984 ],
985 ]
986 );
987
988 $this->add_group_control(
989 Group_Control_Border::get_type(),
990 [
991 'name' => 'kng_button_border',
992 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__cta .button',
993 ]
994 );
995
996 $this->add_group_control(
997 Group_Control_Box_Shadow::get_type(),
998 [
999 'name' => 'kng_button_shadow',
1000 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__cta .button',
1001 ]
1002 );
1003
1004 $this->end_controls_tab();
1005
1006 $this->start_controls_tab(
1007 'kng_button_style_tab_hover',
1008 [
1009 'label' => esc_html__('Hover', 'king-addons'),
1010 ]
1011 );
1012
1013 $this->add_control(
1014 'kng_button_color_hover',
1015 [
1016 'label' => esc_html__('Hover Text Color', 'king-addons'),
1017 'type' => Controls_Manager::COLOR,
1018 'selectors' => [
1019 '{{WRAPPER}} .king-addons-quick-product-grid__cta .button:hover' => 'color: {{VALUE}};',
1020 ],
1021 ]
1022 );
1023
1024 $this->add_control(
1025 'kng_button_bg_hover',
1026 [
1027 'label' => esc_html__('Hover Background', 'king-addons'),
1028 'type' => Controls_Manager::COLOR,
1029 'selectors' => [
1030 '{{WRAPPER}} .king-addons-quick-product-grid__cta .button:hover' => 'background-color: {{VALUE}};',
1031 ],
1032 ]
1033 );
1034
1035 $this->add_group_control(
1036 Group_Control_Border::get_type(),
1037 [
1038 'name' => 'kng_button_border_hover',
1039 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__cta .button:hover',
1040 ]
1041 );
1042
1043 $this->add_group_control(
1044 Group_Control_Box_Shadow::get_type(),
1045 [
1046 'name' => 'kng_button_shadow_hover',
1047 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__cta .button:hover',
1048 ]
1049 );
1050
1051 $this->end_controls_tab();
1052
1053 $this->end_controls_tabs();
1054
1055 $this->add_control(
1056 'kng_button_radius',
1057 [
1058 'label' => esc_html__('Border Radius', 'king-addons'),
1059 'type' => Controls_Manager::SLIDER,
1060 'size_units' => ['px', '%'],
1061 'range' => [
1062 'px' => ['min' => 0, 'max' => 40],
1063 '%' => ['min' => 0, 'max' => 100],
1064 ],
1065 'selectors' => [
1066 '{{WRAPPER}} .king-addons-quick-product-grid__cta .button' => 'border-radius: {{SIZE}}{{UNIT}};',
1067 ],
1068 ]
1069 );
1070
1071 $this->end_controls_section();
1072 }
1073
1074 /**
1075 * View cart link styles (WooCommerce injects `.added_to_cart` link after AJAX add to cart).
1076 *
1077 * @return void
1078 */
1079 protected function register_style_view_cart_controls(): void
1080 {
1081 $this->start_controls_section(
1082 'kng_style_view_cart_section',
1083 [
1084 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('View Cart Link', 'king-addons'),
1085 'tab' => Controls_Manager::TAB_STYLE,
1086 ]
1087 );
1088
1089 $this->add_responsive_control(
1090 'kng_view_cart_gap',
1091 [
1092 'label' => esc_html__('Spacing From Button', 'king-addons'),
1093 'type' => Controls_Manager::SLIDER,
1094 'size_units' => ['px'],
1095 'range' => [
1096 'px' => ['min' => 0, 'max' => 60],
1097 ],
1098 'selectors' => [
1099 '{{WRAPPER}} .king-addons-quick-product-grid__cta .king-addons-quick-product-grid__add-to-cart + .added_to_cart' => 'margin-left: {{SIZE}}{{UNIT}};',
1100 ],
1101 ]
1102 );
1103
1104 $this->add_group_control(
1105 Group_Control_Typography::get_type(),
1106 [
1107 'name' => 'kng_view_cart_typography',
1108 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__cta .added_to_cart',
1109 ]
1110 );
1111
1112 $this->add_responsive_control(
1113 'kng_view_cart_padding',
1114 [
1115 'label' => esc_html__('Padding', 'king-addons'),
1116 'type' => Controls_Manager::DIMENSIONS,
1117 'size_units' => ['px', 'em'],
1118 'selectors' => [
1119 '{{WRAPPER}} .king-addons-quick-product-grid__cta .added_to_cart' => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
1120 ],
1121 ]
1122 );
1123
1124 $this->start_controls_tabs('kng_view_cart_style_tabs');
1125
1126 $this->start_controls_tab(
1127 'kng_view_cart_style_tab_normal',
1128 [
1129 'label' => esc_html__('Normal', 'king-addons'),
1130 ]
1131 );
1132
1133 $this->add_control(
1134 'kng_view_cart_color',
1135 [
1136 'label' => esc_html__('Text Color', 'king-addons'),
1137 'type' => Controls_Manager::COLOR,
1138 'selectors' => [
1139 '{{WRAPPER}} .king-addons-quick-product-grid__cta .added_to_cart' => 'color: {{VALUE}};',
1140 ],
1141 ]
1142 );
1143
1144 $this->add_control(
1145 'kng_view_cart_bg',
1146 [
1147 'label' => esc_html__('Background', 'king-addons'),
1148 'type' => Controls_Manager::COLOR,
1149 'selectors' => [
1150 '{{WRAPPER}} .king-addons-quick-product-grid__cta .added_to_cart' => 'background-color: {{VALUE}};',
1151 ],
1152 ]
1153 );
1154
1155 $this->add_group_control(
1156 Group_Control_Border::get_type(),
1157 [
1158 'name' => 'kng_view_cart_border',
1159 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__cta .added_to_cart',
1160 ]
1161 );
1162
1163 $this->add_group_control(
1164 Group_Control_Box_Shadow::get_type(),
1165 [
1166 'name' => 'kng_view_cart_shadow',
1167 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__cta .added_to_cart',
1168 ]
1169 );
1170
1171 $this->end_controls_tab();
1172
1173 $this->start_controls_tab(
1174 'kng_view_cart_style_tab_hover',
1175 [
1176 'label' => esc_html__('Hover', 'king-addons'),
1177 ]
1178 );
1179
1180 $this->add_control(
1181 'kng_view_cart_color_hover',
1182 [
1183 'label' => esc_html__('Hover Text Color', 'king-addons'),
1184 'type' => Controls_Manager::COLOR,
1185 'selectors' => [
1186 '{{WRAPPER}} .king-addons-quick-product-grid__cta .added_to_cart:hover' => 'color: {{VALUE}};',
1187 ],
1188 ]
1189 );
1190
1191 $this->add_control(
1192 'kng_view_cart_bg_hover',
1193 [
1194 'label' => esc_html__('Hover Background', 'king-addons'),
1195 'type' => Controls_Manager::COLOR,
1196 'selectors' => [
1197 '{{WRAPPER}} .king-addons-quick-product-grid__cta .added_to_cart:hover' => 'background-color: {{VALUE}};',
1198 ],
1199 ]
1200 );
1201
1202 $this->add_group_control(
1203 Group_Control_Border::get_type(),
1204 [
1205 'name' => 'kng_view_cart_border_hover',
1206 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__cta .added_to_cart:hover',
1207 ]
1208 );
1209
1210 $this->add_group_control(
1211 Group_Control_Box_Shadow::get_type(),
1212 [
1213 'name' => 'kng_view_cart_shadow_hover',
1214 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__cta .added_to_cart:hover',
1215 ]
1216 );
1217
1218 $this->end_controls_tab();
1219
1220 $this->end_controls_tabs();
1221
1222 $this->add_control(
1223 'kng_view_cart_radius',
1224 [
1225 'label' => esc_html__('Border Radius', 'king-addons'),
1226 'type' => Controls_Manager::SLIDER,
1227 'size_units' => ['px', '%'],
1228 'range' => [
1229 'px' => ['min' => 0, 'max' => 40],
1230 '%' => ['min' => 0, 'max' => 100],
1231 ],
1232 'selectors' => [
1233 '{{WRAPPER}} .king-addons-quick-product-grid__cta .added_to_cart' => 'border-radius: {{SIZE}}{{UNIT}};',
1234 ],
1235 ]
1236 );
1237
1238 $this->end_controls_section();
1239 }
1240
1241 /**
1242 * Set external query arguments from filters.
1243 *
1244 * @param array<string, mixed> $args Query arguments.
1245 *
1246 * @return void
1247 */
1248 public function set_external_query_args(array $args): void
1249 {
1250 $this->external_query_args = $args;
1251 }
1252
1253 /**
1254 * Build query.
1255 *
1256 * @param array<string, mixed> $settings Settings.
1257 *
1258 * @return WP_Query
1259 */
1260 protected function build_query(array $settings): WP_Query
1261 {
1262 $per_page = !empty($settings['kng_products_per_page']) ? (int) $settings['kng_products_per_page'] : 6;
1263 $per_page = min($per_page, 12);
1264
1265 $orderby_setting = $settings['kng_orderby'] ?? 'date';
1266 $meta_key = $this->get_orderby_meta_key($orderby_setting);
1267
1268 $args = [
1269 'post_type' => 'product',
1270 'posts_per_page' => $per_page,
1271 'order' => $settings['kng_order'] ?? 'DESC',
1272 'orderby' => $this->map_orderby($orderby_setting),
1273 'post_status' => 'publish',
1274 ];
1275
1276 if ($meta_key) {
1277 $args['meta_key'] = $meta_key;
1278 }
1279
1280 $args = $this->apply_product_filters($args, $settings);
1281
1282 if (!empty($this->external_query_args)) {
1283 $args = $this->merge_query_parts($args, $this->external_query_args);
1284 }
1285
1286 // Categories: gated for pro; ignored in free.
1287
1288 return new WP_Query($args);
1289 }
1290
1291 /**
1292 * Map orderby.
1293 *
1294 * @param string $orderby Orderby.
1295 *
1296 * @return string
1297 */
1298 protected function map_orderby(string $orderby): string
1299 {
1300 $map = [
1301 'price' => 'meta_value_num',
1302 'popularity' => 'meta_value_num',
1303 'rating' => 'meta_value_num',
1304 'rand' => 'rand',
1305 'title' => 'title',
1306 'date' => 'date',
1307 ];
1308
1309 return $map[$orderby] ?? 'date';
1310 }
1311
1312 /**
1313 * Return meta key for meta-based orderby.
1314 *
1315 * @param string $orderby Orderby value.
1316 *
1317 * @return string|null
1318 */
1319 protected function get_orderby_meta_key(string $orderby): ?string
1320 {
1321 $map = [
1322 'price' => '_price',
1323 'popularity' => 'total_sales',
1324 'rating' => '_wc_average_rating',
1325 ];
1326
1327 return $map[$orderby] ?? null;
1328 }
1329
1330 /**
1331 * Apply product query filters (include/exclude, on-sale, featured, in-stock).
1332 *
1333 * @param array<string, mixed> $args Query args.
1334 * @param array<string, mixed> $settings Widget settings.
1335 *
1336 * @return array<string, mixed>
1337 */
1338 protected function apply_product_filters(array $args, array $settings): array
1339 {
1340 $filter_args = [];
1341
1342 $include_ids = $this->parse_id_list($settings['kng_include_products'] ?? '');
1343 $exclude_ids = $this->parse_id_list($settings['kng_exclude_products'] ?? '');
1344
1345 $post_in = $include_ids;
1346 $post_in_filter_applied = !empty($include_ids);
1347
1348 if (($settings['kng_on_sale'] ?? '') === 'yes') {
1349 $sale_ids = function_exists('wc_get_product_ids_on_sale') ? wc_get_product_ids_on_sale() : [];
1350 $post_in_filter_applied = true;
1351 $post_in = $post_in ? array_values(array_intersect($post_in, $sale_ids)) : $sale_ids;
1352 }
1353
1354 if ($post_in_filter_applied) {
1355 $filter_args['post__in'] = !empty($post_in) ? $post_in : [0];
1356 }
1357
1358 if (!empty($exclude_ids)) {
1359 $filter_args['post__not_in'] = $exclude_ids;
1360 }
1361
1362 $tax_query = [];
1363 if (($settings['kng_featured_only'] ?? '') === 'yes') {
1364 $tax_query[] = [
1365 'taxonomy' => 'product_visibility',
1366 'field' => 'name',
1367 'terms' => ['featured'],
1368 'operator' => 'IN',
1369 ];
1370 }
1371
1372 if (!empty($tax_query)) {
1373 $filter_args['tax_query'] = $tax_query;
1374 }
1375
1376 $meta_query = [];
1377 if (($settings['kng_in_stock'] ?? '') === 'yes') {
1378 $meta_query[] = [
1379 'key' => '_stock_status',
1380 'value' => 'instock',
1381 ];
1382 }
1383
1384 if (!empty($meta_query)) {
1385 $filter_args['meta_query'] = $meta_query;
1386 }
1387
1388 return $this->merge_query_parts($args, $filter_args);
1389 }
1390
1391 /**
1392 * Parse a comma-separated list of IDs.
1393 *
1394 * @param string $raw Raw input.
1395 *
1396 * @return array<int, int>
1397 */
1398 protected function parse_id_list(string $raw): array
1399 {
1400 if ('' === trim($raw)) {
1401 return [];
1402 }
1403
1404 $ids = array_filter(array_map('absint', array_map('trim', explode(',', $raw))));
1405
1406 return array_values(array_unique($ids));
1407 }
1408
1409 /**
1410 * Merge query arguments, preserving tax/meta queries and combining post__in/not_in.
1411 *
1412 * @param array<string, mixed> $args Base query args.
1413 * @param array<string, mixed> $extra Extra query args.
1414 *
1415 * @return array<string, mixed>
1416 */
1417 protected function merge_query_parts(array $args, array $extra): array
1418 {
1419 $tax_query = $args['tax_query'] ?? [];
1420 $meta_query = $args['meta_query'] ?? [];
1421 $post_in = $args['post__in'] ?? [];
1422 $post_in_filter_applied = array_key_exists('post__in', $args);
1423 $post_not_in = $args['post__not_in'] ?? [];
1424
1425 $extra_tax_query = $extra['tax_query'] ?? [];
1426 $extra_meta_query = $extra['meta_query'] ?? [];
1427 $extra_has_post_in = array_key_exists('post__in', $extra);
1428 $extra_post_in = $extra_has_post_in ? (array) $extra['post__in'] : [];
1429 $extra_post_not_in = $extra['post__not_in'] ?? [];
1430
1431 unset($extra['tax_query'], $extra['meta_query'], $extra['post__in'], $extra['post__not_in']);
1432
1433 $args = array_merge($args, $extra);
1434
1435 if ($extra_has_post_in) {
1436 $post_in_filter_applied = true;
1437 $post_in = !empty($post_in)
1438 ? array_values(array_intersect($post_in, $extra_post_in))
1439 : $extra_post_in;
1440 }
1441
1442 if ($post_in_filter_applied) {
1443 $post_in = array_values(array_unique(array_map('absint', (array) $post_in)));
1444 if (count($post_in) > 1 && in_array(0, $post_in, true)) {
1445 $post_in = array_values(array_diff($post_in, [0]));
1446 }
1447 if (empty($post_in)) {
1448 $post_in = [0];
1449 }
1450 $args['post__in'] = $post_in;
1451 }
1452
1453 $post_not_in = array_merge((array) $post_not_in, (array) $extra_post_not_in);
1454 if (!empty($post_not_in)) {
1455 $post_not_in = array_values(array_unique(array_map('absint', $post_not_in)));
1456 $args['post__not_in'] = $post_not_in;
1457 }
1458
1459 $tax_query = array_merge((array) $tax_query, (array) $extra_tax_query);
1460 if (!empty($tax_query)) {
1461 $args['tax_query'] = $this->normalize_query_relation($tax_query);
1462 }
1463
1464 $meta_query = array_merge((array) $meta_query, (array) $extra_meta_query);
1465 if (!empty($meta_query)) {
1466 $args['meta_query'] = $this->normalize_query_relation($meta_query);
1467 }
1468
1469 return $args;
1470 }
1471
1472 /**
1473 * Ensure relation for tax/meta query arrays.
1474 *
1475 * @param array<int|string, mixed> $query Query array.
1476 *
1477 * @return array<int|string, mixed>
1478 */
1479 protected function normalize_query_relation(array $query): array
1480 {
1481 $conditions = array_filter($query, 'is_array');
1482 if (count($conditions) > 1 && empty($query['relation'])) {
1483 $query['relation'] = 'AND';
1484 }
1485
1486 return $query;
1487 }
1488
1489 /**
1490 * Render card.
1491 *
1492 * @param array<string, mixed> $settings Settings.
1493 * @param \WC_Product $product Product.
1494 *
1495 * @return void
1496 */
1497 protected function render_card(array $settings, \WC_Product $product): void
1498 {
1499 $card_classes = ['king-addons-quick-product-grid__card'];
1500 $animation = $settings['kng_hover_animation'] ?? 'none';
1501 if ($animation !== 'none') {
1502 $card_classes[] = 'is-anim-' . sanitize_html_class((string) $animation);
1503 }
1504
1505 $show_rating = ($settings['kng_show_rating'] ?? 'yes') === 'yes';
1506 $show_price = ($settings['kng_show_price'] ?? 'yes') === 'yes';
1507 $show_excerpt = ($settings['kng_show_excerpt'] ?? 'yes') === 'yes';
1508 $show_button = ($settings['kng_show_add_to_cart'] ?? 'yes') === 'yes';
1509 $show_badge = ($settings['kng_show_badge'] ?? 'yes') === 'yes';
1510 $badge_text = !empty($settings['kng_badge_text']) ? (string) $settings['kng_badge_text'] : esc_html__('Sale', 'king-addons');
1511 $excerpt_length = !empty($settings['kng_excerpt_length']) ? max(1, (int) $settings['kng_excerpt_length']) : 16;
1512
1513 ?>
1514 <div class="king-addons-quick-product-grid__item">
1515 <article class="<?php echo esc_attr(implode(' ', $card_classes)); ?>">
1516 <div class="king-addons-quick-product-grid__media">
1517 <a href="<?php the_permalink(); ?>" class="king-addons-quick-product-grid__link">
1518 <?php echo $this->get_product_image_html($product, $settings); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
1519 </a>
1520 <?php if ($show_badge && $product->is_on_sale()) : ?>
1521 <span class="king-addons-quick-product-grid__badge"><?php echo esc_html($badge_text); ?></span>
1522 <?php endif; ?>
1523 </div>
1524
1525 <div class="king-addons-quick-product-grid__body">
1526 <h3 class="king-addons-quick-product-grid__title">
1527 <a href="<?php the_permalink(); ?>"><?php echo esc_html($product->get_name()); ?></a>
1528 </h3>
1529
1530 <?php if ($show_rating && wc_review_ratings_enabled()) : ?>
1531 <div class="king-addons-quick-product-grid__rating">
1532 <?php echo wc_get_rating_html($product->get_average_rating()); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
1533 </div>
1534 <?php endif; ?>
1535
1536 <?php if ($show_price) : ?>
1537 <div class="king-addons-quick-product-grid__price">
1538 <?php echo $product->get_price_html(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
1539 </div>
1540 <?php endif; ?>
1541
1542 <?php if ($show_excerpt) : ?>
1543 <div class="king-addons-quick-product-grid__excerpt">
1544 <?php echo wp_kses_post(wp_trim_words($product->get_short_description(), $excerpt_length)); ?>
1545 </div>
1546 <?php endif; ?>
1547
1548 <?php if ($show_button) : ?>
1549 <div class="king-addons-quick-product-grid__cta">
1550 <?php echo $this->render_add_to_cart_button($product); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
1551 </div>
1552 <?php endif; ?>
1553 </div>
1554 </article>
1555 </div>
1556 <?php
1557 }
1558
1559 /**
1560 * Get product image HTML with Elementor image size control.
1561 *
1562 * @param \WC_Product $product Product instance.
1563 * @param array<string, mixed> $settings Widget settings.
1564 *
1565 * @return string
1566 */
1567 protected function get_product_image_html(\WC_Product $product, array $settings): string
1568 {
1569 $image_id = $product->get_image_id();
1570
1571 if ($image_id) {
1572 $image_html = Core::getAttachmentImageHTML($settings, 'kng_image_size', (int) $image_id);
1573 if (!empty($image_html)) {
1574 return $image_html;
1575 }
1576 }
1577
1578 $fallback_size = $settings['kng_image_size_size'] ?? 'medium';
1579
1580 return $product->get_image($fallback_size);
1581 }
1582
1583 /**
1584 * Render a controlled Add to Cart button without relying on Woo shortcode.
1585 *
1586 * @param \WC_Product $product Product instance.
1587 *
1588 * @return string
1589 */
1590 protected function render_add_to_cart_button(\WC_Product $product): string
1591 {
1592 if (!$product->is_purchasable() || !$product->is_in_stock()) {
1593 return '<span class="king-addons-quick-product-grid__add-to-cart is-disabled">' . esc_html__('Unavailable', 'king-addons') . '</span>';
1594 }
1595
1596 $classes = [
1597 'king-addons-quick-product-grid__add-to-cart',
1598 'button',
1599 'product_type_' . $product->get_type(),
1600 ];
1601
1602 if ($product->supports('ajax_add_to_cart')) {
1603 $classes[] = 'ajax_add_to_cart';
1604 $classes[] = 'add_to_cart_button';
1605 }
1606
1607 $attributes = [
1608 'href' => $product->add_to_cart_url(),
1609 'data-quantity' => 1,
1610 'data-product_id' => $product->get_id(),
1611 'data-product_sku' => $product->get_sku(),
1612 'rel' => 'nofollow',
1613 'class' => implode(' ', array_filter($classes)),
1614 'aria-label' => wp_strip_all_tags($product->add_to_cart_description()),
1615 ];
1616
1617 return '<a ' . wc_implode_html_attributes($attributes) . '><span class="king-addons-quick-product-grid__add-to-cart-label">' . esc_html($product->add_to_cart_text()) . '</span></a>';
1618 }
1619
1620 /**
1621 * Wrapper classes.
1622 *
1623 * @param array<string, mixed> $settings Settings.
1624 *
1625 * @return array<int, string>
1626 */
1627 protected function get_wrapper_classes(array $settings): array
1628 {
1629 $classes = ['king-addons-quick-product-grid'];
1630 $columns = $settings['kng_columns'] ?? 3;
1631 $classes[] = 'king-addons-quick-product-grid--cols-' . sanitize_html_class((string) $columns);
1632 return $classes;
1633 }
1634
1635 /**
1636 * Wrapper inline style.
1637 *
1638 * @param array<string, mixed> $settings Settings.
1639 *
1640 * @return string
1641 */
1642 protected function get_wrapper_style(array $settings): string
1643 {
1644 $parts = [];
1645 if (isset($settings['kng_grid_gap']['size'])) {
1646 $parts[] = '--kng-quick-product-grid-gap:' . (float) $settings['kng_grid_gap']['size'] . ($settings['kng_grid_gap']['unit'] ?? 'px') . ';';
1647 }
1648
1649 return implode(' ', $parts);
1650 }
1651
1652 /**
1653 * Pro notice.
1654 *
1655 * @return void
1656 */
1657 public function register_pro_notice_controls(): void
1658 {
1659 if (!king_addons_freemius()->can_use_premium_code__premium_only()) {
1660 Core::renderProFeaturesSection($this, '', Controls_Manager::RAW_HTML, 'quick-product-grid', [
1661 'Category filter and higher limits',
1662 'Advanced hover animations and skins',
1663 'Advanced badges and ribbons',
1664 'Advanced CTA layouts',
1665 ]);
1666 }
1667 }
1668 }
1669