PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.83
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.83
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 51.1.45 51.1.46 51.1.47 51.1.49 All 37 releases
king-addons / includes / widgets / Quick_Product_Grid / Quick_Product_Grid.php

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

1,667 lines 53.2 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:bug@kingaddons.com?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
194 if (!$query->have_posts()) {
195 wp_reset_postdata();
196 return;
197 }
198
199 $wrapper_classes = $this->get_wrapper_classes($settings);
200 $wrapper_style = $this->get_wrapper_style($settings);
201 $wrapper_handle = self::FILTER_WRAPPER_HANDLE;
202
203 $this->add_render_attribute($wrapper_handle, 'class', $wrapper_classes);
204
205 if ('' !== $wrapper_style) {
206 $this->add_render_attribute($wrapper_handle, 'style', $wrapper_style);
207 }
208
209 ?>
210 <div <?php echo $this->get_render_attribute_string($wrapper_handle); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
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 'selectors' => [
372 '{{WRAPPER}} .king-addons-quick-product-grid__grid' => 'grid-template-columns: repeat({{VALUE}}, minmax(0, 1fr));',
373 ],
374 ]
375 );
376
377 $this->add_responsive_control(
378 'kng_grid_gap',
379 [
380 'label' => esc_html__('Gap', 'king-addons'),
381 'type' => Controls_Manager::SLIDER,
382 'size_units' => ['px'],
383 'range' => [
384 'px' => [
385 'min' => 0,
386 'max' => 80,
387 ],
388 ],
389 'default' => [
390 'size' => 20,
391 'unit' => 'px',
392 ],
393 'selectors' => [
394 '{{WRAPPER}} .king-addons-quick-product-grid__grid' => 'gap: {{SIZE}}{{UNIT}};',
395 ],
396 ]
397 );
398
399 $this->end_controls_section();
400 }
401
402 /**
403 * Display controls.
404 *
405 * @return void
406 */
407 protected function register_display_controls(): void
408 {
409 $this->start_controls_section(
410 'kng_display_section',
411 [
412 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Display', 'king-addons'),
413 'tab' => Controls_Manager::TAB_CONTENT,
414 ]
415 );
416
417 $this->add_control(
418 'kng_show_rating',
419 [
420 'label' => esc_html__('Show Rating', 'king-addons'),
421 'type' => Controls_Manager::SWITCHER,
422 'return_value' => 'yes',
423 'default' => 'yes',
424 ]
425 );
426
427 $this->add_control(
428 'kng_show_price',
429 [
430 'label' => esc_html__('Show Price', 'king-addons'),
431 'type' => Controls_Manager::SWITCHER,
432 'return_value' => 'yes',
433 'default' => 'yes',
434 ]
435 );
436
437 $this->add_control(
438 'kng_show_excerpt',
439 [
440 'label' => esc_html__('Show Short Description', 'king-addons'),
441 'type' => Controls_Manager::SWITCHER,
442 'return_value' => 'yes',
443 'default' => 'yes',
444 ]
445 );
446
447 $this->add_control(
448 'kng_show_add_to_cart',
449 [
450 'label' => esc_html__('Show Add to Cart', 'king-addons'),
451 'type' => Controls_Manager::SWITCHER,
452 'return_value' => 'yes',
453 'default' => 'yes',
454 ]
455 );
456
457 $this->add_control(
458 'kng_show_badge',
459 [
460 'label' => esc_html__('Show Sale Badge', 'king-addons'),
461 'type' => Controls_Manager::SWITCHER,
462 'return_value' => 'yes',
463 'default' => 'yes',
464 ]
465 );
466
467 $this->add_control(
468 'kng_badge_text',
469 [
470 'label' => esc_html__('Sale Badge Text', 'king-addons'),
471 'type' => Controls_Manager::TEXT,
472 'default' => esc_html__('Sale', 'king-addons'),
473 'condition' => [
474 'kng_show_badge' => 'yes',
475 ],
476 ]
477 );
478
479 $this->add_control(
480 'kng_excerpt_length',
481 [
482 'label' => esc_html__('Excerpt Length (words)', 'king-addons'),
483 'type' => Controls_Manager::NUMBER,
484 'min' => 5,
485 'max' => 60,
486 'step' => 1,
487 'default' => 16,
488 'condition' => [
489 'kng_show_excerpt' => 'yes',
490 ],
491 ]
492 );
493
494 $this->add_group_control(
495 Group_Control_Image_Size::get_type(),
496 [
497 'name' => 'kng_image_size',
498 'default' => 'medium',
499 ]
500 );
501
502 $this->end_controls_section();
503 }
504
505 /**
506 * Card style controls.
507 *
508 * @return void
509 */
510 protected function register_style_card_controls(): void
511 {
512 $this->start_controls_section(
513 'kng_style_card_section',
514 [
515 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Card', 'king-addons'),
516 'tab' => Controls_Manager::TAB_STYLE,
517 ]
518 );
519
520 $this->add_control(
521 'kng_card_background',
522 [
523 'label' => esc_html__('Background', 'king-addons'),
524 'type' => Controls_Manager::COLOR,
525 'selectors' => [
526 '{{WRAPPER}} .king-addons-quick-product-grid__card' => 'background-color: {{VALUE}};',
527 ],
528 ]
529 );
530
531 $this->add_group_control(
532 Group_Control_Border::get_type(),
533 [
534 'name' => 'kng_card_border',
535 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__card',
536 ]
537 );
538
539 $this->add_control(
540 'kng_card_radius',
541 [
542 'label' => esc_html__('Border Radius', 'king-addons'),
543 'type' => Controls_Manager::SLIDER,
544 'size_units' => ['px', '%'],
545 'range' => [
546 'px' => ['min' => 0, 'max' => 50],
547 '%' => ['min' => 0, 'max' => 100],
548 ],
549 'selectors' => [
550 '{{WRAPPER}} .king-addons-quick-product-grid__card' => 'border-radius: {{SIZE}}{{UNIT}};',
551 ],
552 ]
553 );
554
555 $this->add_group_control(
556 Group_Control_Box_Shadow::get_type(),
557 [
558 'name' => 'kng_card_shadow',
559 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__card',
560 ]
561 );
562
563 $this->add_control(
564 'kng_card_shadow_disable',
565 [
566 'label' => esc_html__('Disable Box Shadow', 'king-addons'),
567 'type' => Controls_Manager::SWITCHER,
568 'label_on' => esc_html__('Yes', 'king-addons'),
569 'label_off' => esc_html__('No', 'king-addons'),
570 'return_value' => 'yes',
571 'selectors' => [
572 '{{WRAPPER}} .king-addons-quick-product-grid__card' => 'box-shadow: none !important;',
573 ],
574 ]
575 );
576
577 $this->add_control(
578 'kng_card_shadow_hover_disable',
579 [
580 'label' => esc_html__('Disable Box Shadow on Hover', 'king-addons'),
581 'type' => Controls_Manager::SWITCHER,
582 'label_on' => esc_html__('Yes', 'king-addons'),
583 'label_off' => esc_html__('No', 'king-addons'),
584 'return_value' => 'yes',
585 'selectors' => [
586 '{{WRAPPER}} .king-addons-quick-product-grid__card:hover' => 'box-shadow: none !important;',
587 ],
588 ]
589 );
590
591 $this->add_control(
592 'kng_card_padding',
593 [
594 'label' => esc_html__('Padding', 'king-addons'),
595 'type' => Controls_Manager::DIMENSIONS,
596 'size_units' => ['px', '%', 'em'],
597 'selectors' => [
598 '{{WRAPPER}} .king-addons-quick-product-grid__card' => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
599 ],
600 ]
601 );
602
603 $this->end_controls_section();
604 }
605
606 /**
607 * Text styles.
608 *
609 * @return void
610 */
611 protected function register_style_text_controls(): void
612 {
613 $this->start_controls_section(
614 'kng_style_text_section',
615 [
616 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Text', 'king-addons'),
617 'tab' => Controls_Manager::TAB_STYLE,
618 ]
619 );
620
621 $this->add_group_control(
622 Group_Control_Typography::get_type(),
623 [
624 'name' => 'kng_title_typography',
625 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__title',
626 ]
627 );
628
629 $this->add_control(
630 'kng_title_color',
631 [
632 'label' => esc_html__('Title Color', 'king-addons'),
633 'type' => Controls_Manager::COLOR,
634 'selectors' => [
635 '{{WRAPPER}} .king-addons-quick-product-grid__title a' => 'color: {{VALUE}};',
636 ],
637 ]
638 );
639
640 $this->add_group_control(
641 Group_Control_Typography::get_type(),
642 [
643 'name' => 'kng_price_typography',
644 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__price',
645 ]
646 );
647
648 $this->add_control(
649 'kng_price_color',
650 [
651 'label' => esc_html__('Price Color', 'king-addons'),
652 'type' => Controls_Manager::COLOR,
653 'selectors' => [
654 '{{WRAPPER}} .king-addons-quick-product-grid__price' => 'color: {{VALUE}};',
655 ],
656 ]
657 );
658
659 $this->add_group_control(
660 Group_Control_Typography::get_type(),
661 [
662 'name' => 'kng_excerpt_typography',
663 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__excerpt',
664 ]
665 );
666
667 $this->add_control(
668 'kng_excerpt_color',
669 [
670 'label' => esc_html__('Excerpt Color', 'king-addons'),
671 'type' => Controls_Manager::COLOR,
672 'selectors' => [
673 '{{WRAPPER}} .king-addons-quick-product-grid__excerpt' => 'color: {{VALUE}};',
674 ],
675 ]
676 );
677
678 $this->end_controls_section();
679 }
680
681 /**
682 * Rating styles.
683 *
684 * @return void
685 */
686 protected function register_style_rating_controls(): void
687 {
688 $this->start_controls_section(
689 'kng_style_rating_section',
690 [
691 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Rating', 'king-addons'),
692 'tab' => Controls_Manager::TAB_STYLE,
693 'condition' => [
694 'kng_show_rating' => 'yes',
695 ],
696 ]
697 );
698
699 $this->add_control(
700 'kng_rating_color_empty',
701 [
702 'label' => esc_html__('Empty Color', 'king-addons'),
703 'type' => Controls_Manager::COLOR,
704 'selectors' => [
705 '{{WRAPPER}} .king-addons-quick-product-grid__rating .star-rating' => 'color: {{VALUE}};',
706 ],
707 ]
708 );
709
710 $this->add_control(
711 'kng_rating_color_filled',
712 [
713 'label' => esc_html__('Filled Color', 'king-addons'),
714 'type' => Controls_Manager::COLOR,
715 'selectors' => [
716 '{{WRAPPER}} .king-addons-quick-product-grid__rating .star-rating span' => 'color: {{VALUE}};',
717 ],
718 ]
719 );
720
721 $this->add_control(
722 'kng_rating_size',
723 [
724 'label' => esc_html__('Size', 'king-addons'),
725 'type' => Controls_Manager::SLIDER,
726 'size_units' => ['px'],
727 'range' => [
728 'px' => ['min' => 10, 'max' => 32],
729 ],
730 'selectors' => [
731 '{{WRAPPER}} .king-addons-quick-product-grid__rating .star-rating' => 'font-size: {{SIZE}}{{UNIT}};',
732 ],
733 ]
734 );
735
736 $this->end_controls_section();
737 }
738
739 /**
740 * Sale badge styles.
741 *
742 * @return void
743 */
744 protected function register_style_badge_controls(): void
745 {
746 $this->start_controls_section(
747 'kng_style_badge_section',
748 [
749 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Sale Badge', 'king-addons'),
750 'tab' => Controls_Manager::TAB_STYLE,
751 'condition' => [
752 'kng_show_badge' => 'yes',
753 ],
754 ]
755 );
756
757 $this->add_group_control(
758 Group_Control_Typography::get_type(),
759 [
760 'name' => 'kng_badge_typography',
761 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__badge',
762 ]
763 );
764
765 $this->add_control(
766 'kng_badge_color',
767 [
768 'label' => esc_html__('Text Color', 'king-addons'),
769 'type' => Controls_Manager::COLOR,
770 'selectors' => [
771 '{{WRAPPER}} .king-addons-quick-product-grid__badge' => 'color: {{VALUE}};',
772 ],
773 ]
774 );
775
776 $this->add_control(
777 'kng_badge_background',
778 [
779 'label' => esc_html__('Background', 'king-addons'),
780 'type' => Controls_Manager::COLOR,
781 'selectors' => [
782 '{{WRAPPER}} .king-addons-quick-product-grid__badge' => 'background-color: {{VALUE}};',
783 ],
784 ]
785 );
786
787 $this->add_group_control(
788 Group_Control_Border::get_type(),
789 [
790 'name' => 'kng_badge_border',
791 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__badge',
792 ]
793 );
794
795 $this->add_group_control(
796 Group_Control_Box_Shadow::get_type(),
797 [
798 'name' => 'kng_badge_shadow',
799 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__badge',
800 ]
801 );
802
803 $this->add_control(
804 'kng_badge_radius',
805 [
806 'label' => esc_html__('Border Radius', 'king-addons'),
807 'type' => Controls_Manager::SLIDER,
808 'size_units' => ['px', '%'],
809 'range' => [
810 'px' => ['min' => 0, 'max' => 50],
811 '%' => ['min' => 0, 'max' => 100],
812 ],
813 'selectors' => [
814 '{{WRAPPER}} .king-addons-quick-product-grid__badge' => 'border-radius: {{SIZE}}{{UNIT}};',
815 ],
816 ]
817 );
818
819 $this->add_responsive_control(
820 'kng_badge_padding',
821 [
822 'label' => esc_html__('Padding', 'king-addons'),
823 'type' => Controls_Manager::DIMENSIONS,
824 'size_units' => ['px', 'em'],
825 'selectors' => [
826 '{{WRAPPER}} .king-addons-quick-product-grid__badge' => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
827 ],
828 ]
829 );
830
831 $this->add_responsive_control(
832 'kng_badge_offset_top',
833 [
834 'label' => esc_html__('Offset Top', 'king-addons'),
835 'type' => Controls_Manager::SLIDER,
836 'size_units' => ['px'],
837 'range' => [
838 'px' => ['min' => 0, 'max' => 80],
839 ],
840 'selectors' => [
841 '{{WRAPPER}} .king-addons-quick-product-grid__badge' => 'top: {{SIZE}}{{UNIT}};',
842 ],
843 ]
844 );
845
846 $this->add_responsive_control(
847 'kng_badge_offset_left',
848 [
849 'label' => esc_html__('Offset Left', 'king-addons'),
850 'type' => Controls_Manager::SLIDER,
851 'size_units' => ['px'],
852 'range' => [
853 'px' => ['min' => 0, 'max' => 80],
854 ],
855 'selectors' => [
856 '{{WRAPPER}} .king-addons-quick-product-grid__badge' => 'left: {{SIZE}}{{UNIT}};',
857 ],
858 ]
859 );
860
861 $this->end_controls_section();
862 }
863
864 /**
865 * Button styles.
866 *
867 * @return void
868 */
869 protected function register_style_button_controls(): void
870 {
871 $this->start_controls_section(
872 'kng_style_button_section',
873 [
874 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('Add to Cart', 'king-addons'),
875 'tab' => Controls_Manager::TAB_STYLE,
876 ]
877 );
878
879 $this->add_responsive_control(
880 'kng_cta_alignment',
881 [
882 'label' => esc_html__('Alignment', 'king-addons'),
883 'type' => Controls_Manager::CHOOSE,
884 'options' => [
885 'flex-start' => [
886 'title' => esc_html__('Start', 'king-addons'),
887 'icon' => 'eicon-h-align-left',
888 ],
889 'center' => [
890 'title' => esc_html__('Center', 'king-addons'),
891 'icon' => 'eicon-h-align-center',
892 ],
893 'flex-end' => [
894 'title' => esc_html__('End', 'king-addons'),
895 'icon' => 'eicon-h-align-right',
896 ],
897 ],
898 'toggle' => false,
899 'default' => 'flex-start',
900 'selectors' => [
901 '{{WRAPPER}} .king-addons-quick-product-grid__cta' => 'justify-content: {{VALUE}};',
902 ],
903 ]
904 );
905
906 $this->add_control(
907 'kng_button_full_width',
908 [
909 'label' => esc_html__('Full Width Button', 'king-addons'),
910 'type' => Controls_Manager::SWITCHER,
911 'return_value' => 'yes',
912 'default' => '',
913 'selectors' => [
914 '{{WRAPPER}} .king-addons-quick-product-grid__cta' => 'width: 100%;',
915 '{{WRAPPER}} .king-addons-quick-product-grid__cta .button' => 'width: 100%; justify-content: center;',
916 ],
917 ]
918 );
919
920 $this->add_group_control(
921 Group_Control_Typography::get_type(),
922 [
923 'name' => 'kng_button_typography',
924 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__cta .button',
925 ]
926 );
927
928 $this->add_responsive_control(
929 'kng_button_padding',
930 [
931 'label' => esc_html__('Padding', 'king-addons'),
932 'type' => Controls_Manager::DIMENSIONS,
933 'size_units' => ['px', 'em'],
934 'selectors' => [
935 '{{WRAPPER}} .king-addons-quick-product-grid__cta .button' => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
936 ],
937 ]
938 );
939
940 $this->add_responsive_control(
941 'kng_button_indicator_size',
942 [
943 'label' => esc_html__('Indicator Size', 'king-addons'),
944 'type' => Controls_Manager::SLIDER,
945 'size_units' => ['px'],
946 'range' => [
947 'px' => ['min' => 8, 'max' => 40],
948 ],
949 'selectors' => [
950 '{{WRAPPER}} .king-addons-quick-product-grid__cta .king-addons-quick-product-grid__add-to-cart' => '--king-addons-atc-indicator-size: {{SIZE}}{{UNIT}};',
951 ],
952 ]
953 );
954
955 $this->start_controls_tabs('kng_button_style_tabs');
956
957 $this->start_controls_tab(
958 'kng_button_style_tab_normal',
959 [
960 'label' => esc_html__('Normal', 'king-addons'),
961 ]
962 );
963
964 $this->add_control(
965 'kng_button_color',
966 [
967 'label' => esc_html__('Text Color', 'king-addons'),
968 'type' => Controls_Manager::COLOR,
969 'selectors' => [
970 '{{WRAPPER}} .king-addons-quick-product-grid__cta .button' => 'color: {{VALUE}};',
971 ],
972 ]
973 );
974
975 $this->add_control(
976 'kng_button_bg',
977 [
978 'label' => esc_html__('Background', 'king-addons'),
979 'type' => Controls_Manager::COLOR,
980 'selectors' => [
981 '{{WRAPPER}} .king-addons-quick-product-grid__cta .button' => 'background-color: {{VALUE}};',
982 ],
983 ]
984 );
985
986 $this->add_group_control(
987 Group_Control_Border::get_type(),
988 [
989 'name' => 'kng_button_border',
990 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__cta .button',
991 ]
992 );
993
994 $this->add_group_control(
995 Group_Control_Box_Shadow::get_type(),
996 [
997 'name' => 'kng_button_shadow',
998 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__cta .button',
999 ]
1000 );
1001
1002 $this->end_controls_tab();
1003
1004 $this->start_controls_tab(
1005 'kng_button_style_tab_hover',
1006 [
1007 'label' => esc_html__('Hover', 'king-addons'),
1008 ]
1009 );
1010
1011 $this->add_control(
1012 'kng_button_color_hover',
1013 [
1014 'label' => esc_html__('Hover Text Color', 'king-addons'),
1015 'type' => Controls_Manager::COLOR,
1016 'selectors' => [
1017 '{{WRAPPER}} .king-addons-quick-product-grid__cta .button:hover' => 'color: {{VALUE}};',
1018 ],
1019 ]
1020 );
1021
1022 $this->add_control(
1023 'kng_button_bg_hover',
1024 [
1025 'label' => esc_html__('Hover Background', 'king-addons'),
1026 'type' => Controls_Manager::COLOR,
1027 'selectors' => [
1028 '{{WRAPPER}} .king-addons-quick-product-grid__cta .button:hover' => 'background-color: {{VALUE}};',
1029 ],
1030 ]
1031 );
1032
1033 $this->add_group_control(
1034 Group_Control_Border::get_type(),
1035 [
1036 'name' => 'kng_button_border_hover',
1037 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__cta .button:hover',
1038 ]
1039 );
1040
1041 $this->add_group_control(
1042 Group_Control_Box_Shadow::get_type(),
1043 [
1044 'name' => 'kng_button_shadow_hover',
1045 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__cta .button:hover',
1046 ]
1047 );
1048
1049 $this->end_controls_tab();
1050
1051 $this->end_controls_tabs();
1052
1053 $this->add_control(
1054 'kng_button_radius',
1055 [
1056 'label' => esc_html__('Border Radius', 'king-addons'),
1057 'type' => Controls_Manager::SLIDER,
1058 'size_units' => ['px', '%'],
1059 'range' => [
1060 'px' => ['min' => 0, 'max' => 40],
1061 '%' => ['min' => 0, 'max' => 100],
1062 ],
1063 'selectors' => [
1064 '{{WRAPPER}} .king-addons-quick-product-grid__cta .button' => 'border-radius: {{SIZE}}{{UNIT}};',
1065 ],
1066 ]
1067 );
1068
1069 $this->end_controls_section();
1070 }
1071
1072 /**
1073 * View cart link styles (WooCommerce injects `.added_to_cart` link after AJAX add to cart).
1074 *
1075 * @return void
1076 */
1077 protected function register_style_view_cart_controls(): void
1078 {
1079 $this->start_controls_section(
1080 'kng_style_view_cart_section',
1081 [
1082 'label' => KING_ADDONS_ELEMENTOR_ICON . esc_html__('View Cart Link', 'king-addons'),
1083 'tab' => Controls_Manager::TAB_STYLE,
1084 ]
1085 );
1086
1087 $this->add_responsive_control(
1088 'kng_view_cart_gap',
1089 [
1090 'label' => esc_html__('Spacing From Button', 'king-addons'),
1091 'type' => Controls_Manager::SLIDER,
1092 'size_units' => ['px'],
1093 'range' => [
1094 'px' => ['min' => 0, 'max' => 60],
1095 ],
1096 'selectors' => [
1097 '{{WRAPPER}} .king-addons-quick-product-grid__cta .king-addons-quick-product-grid__add-to-cart + .added_to_cart' => 'margin-left: {{SIZE}}{{UNIT}};',
1098 ],
1099 ]
1100 );
1101
1102 $this->add_group_control(
1103 Group_Control_Typography::get_type(),
1104 [
1105 'name' => 'kng_view_cart_typography',
1106 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__cta .added_to_cart',
1107 ]
1108 );
1109
1110 $this->add_responsive_control(
1111 'kng_view_cart_padding',
1112 [
1113 'label' => esc_html__('Padding', 'king-addons'),
1114 'type' => Controls_Manager::DIMENSIONS,
1115 'size_units' => ['px', 'em'],
1116 'selectors' => [
1117 '{{WRAPPER}} .king-addons-quick-product-grid__cta .added_to_cart' => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
1118 ],
1119 ]
1120 );
1121
1122 $this->start_controls_tabs('kng_view_cart_style_tabs');
1123
1124 $this->start_controls_tab(
1125 'kng_view_cart_style_tab_normal',
1126 [
1127 'label' => esc_html__('Normal', 'king-addons'),
1128 ]
1129 );
1130
1131 $this->add_control(
1132 'kng_view_cart_color',
1133 [
1134 'label' => esc_html__('Text Color', 'king-addons'),
1135 'type' => Controls_Manager::COLOR,
1136 'selectors' => [
1137 '{{WRAPPER}} .king-addons-quick-product-grid__cta .added_to_cart' => 'color: {{VALUE}};',
1138 ],
1139 ]
1140 );
1141
1142 $this->add_control(
1143 'kng_view_cart_bg',
1144 [
1145 'label' => esc_html__('Background', 'king-addons'),
1146 'type' => Controls_Manager::COLOR,
1147 'selectors' => [
1148 '{{WRAPPER}} .king-addons-quick-product-grid__cta .added_to_cart' => 'background-color: {{VALUE}};',
1149 ],
1150 ]
1151 );
1152
1153 $this->add_group_control(
1154 Group_Control_Border::get_type(),
1155 [
1156 'name' => 'kng_view_cart_border',
1157 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__cta .added_to_cart',
1158 ]
1159 );
1160
1161 $this->add_group_control(
1162 Group_Control_Box_Shadow::get_type(),
1163 [
1164 'name' => 'kng_view_cart_shadow',
1165 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__cta .added_to_cart',
1166 ]
1167 );
1168
1169 $this->end_controls_tab();
1170
1171 $this->start_controls_tab(
1172 'kng_view_cart_style_tab_hover',
1173 [
1174 'label' => esc_html__('Hover', 'king-addons'),
1175 ]
1176 );
1177
1178 $this->add_control(
1179 'kng_view_cart_color_hover',
1180 [
1181 'label' => esc_html__('Hover Text Color', 'king-addons'),
1182 'type' => Controls_Manager::COLOR,
1183 'selectors' => [
1184 '{{WRAPPER}} .king-addons-quick-product-grid__cta .added_to_cart:hover' => 'color: {{VALUE}};',
1185 ],
1186 ]
1187 );
1188
1189 $this->add_control(
1190 'kng_view_cart_bg_hover',
1191 [
1192 'label' => esc_html__('Hover Background', 'king-addons'),
1193 'type' => Controls_Manager::COLOR,
1194 'selectors' => [
1195 '{{WRAPPER}} .king-addons-quick-product-grid__cta .added_to_cart:hover' => 'background-color: {{VALUE}};',
1196 ],
1197 ]
1198 );
1199
1200 $this->add_group_control(
1201 Group_Control_Border::get_type(),
1202 [
1203 'name' => 'kng_view_cart_border_hover',
1204 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__cta .added_to_cart:hover',
1205 ]
1206 );
1207
1208 $this->add_group_control(
1209 Group_Control_Box_Shadow::get_type(),
1210 [
1211 'name' => 'kng_view_cart_shadow_hover',
1212 'selector' => '{{WRAPPER}} .king-addons-quick-product-grid__cta .added_to_cart:hover',
1213 ]
1214 );
1215
1216 $this->end_controls_tab();
1217
1218 $this->end_controls_tabs();
1219
1220 $this->add_control(
1221 'kng_view_cart_radius',
1222 [
1223 'label' => esc_html__('Border Radius', 'king-addons'),
1224 'type' => Controls_Manager::SLIDER,
1225 'size_units' => ['px', '%'],
1226 'range' => [
1227 'px' => ['min' => 0, 'max' => 40],
1228 '%' => ['min' => 0, 'max' => 100],
1229 ],
1230 'selectors' => [
1231 '{{WRAPPER}} .king-addons-quick-product-grid__cta .added_to_cart' => 'border-radius: {{SIZE}}{{UNIT}};',
1232 ],
1233 ]
1234 );
1235
1236 $this->end_controls_section();
1237 }
1238
1239 /**
1240 * Set external query arguments from filters.
1241 *
1242 * @param array<string, mixed> $args Query arguments.
1243 *
1244 * @return void
1245 */
1246 public function set_external_query_args(array $args): void
1247 {
1248 $this->external_query_args = $args;
1249 }
1250
1251 /**
1252 * Build query.
1253 *
1254 * @param array<string, mixed> $settings Settings.
1255 *
1256 * @return WP_Query
1257 */
1258 protected function build_query(array $settings): WP_Query
1259 {
1260 $per_page = !empty($settings['kng_products_per_page']) ? (int) $settings['kng_products_per_page'] : 6;
1261 $per_page = min($per_page, 12);
1262
1263 $orderby_setting = $settings['kng_orderby'] ?? 'date';
1264 $meta_key = $this->get_orderby_meta_key($orderby_setting);
1265
1266 $args = [
1267 'post_type' => 'product',
1268 'posts_per_page' => $per_page,
1269 'order' => $settings['kng_order'] ?? 'DESC',
1270 'orderby' => $this->map_orderby($orderby_setting),
1271 'post_status' => 'publish',
1272 ];
1273
1274 if ($meta_key) {
1275 $args['meta_key'] = $meta_key;
1276 }
1277
1278 $args = $this->apply_product_filters($args, $settings);
1279
1280 if (!empty($this->external_query_args)) {
1281 $args = $this->merge_query_parts($args, $this->external_query_args);
1282 }
1283
1284 // Categories: gated for pro; ignored in free.
1285
1286 return new WP_Query($args);
1287 }
1288
1289 /**
1290 * Map orderby.
1291 *
1292 * @param string $orderby Orderby.
1293 *
1294 * @return string
1295 */
1296 protected function map_orderby(string $orderby): string
1297 {
1298 $map = [
1299 'price' => 'meta_value_num',
1300 'popularity' => 'meta_value_num',
1301 'rating' => 'meta_value_num',
1302 'rand' => 'rand',
1303 'title' => 'title',
1304 'date' => 'date',
1305 ];
1306
1307 return $map[$orderby] ?? 'date';
1308 }
1309
1310 /**
1311 * Return meta key for meta-based orderby.
1312 *
1313 * @param string $orderby Orderby value.
1314 *
1315 * @return string|null
1316 */
1317 protected function get_orderby_meta_key(string $orderby): ?string
1318 {
1319 $map = [
1320 'price' => '_price',
1321 'popularity' => 'total_sales',
1322 'rating' => '_wc_average_rating',
1323 ];
1324
1325 return $map[$orderby] ?? null;
1326 }
1327
1328 /**
1329 * Apply product query filters (include/exclude, on-sale, featured, in-stock).
1330 *
1331 * @param array<string, mixed> $args Query args.
1332 * @param array<string, mixed> $settings Widget settings.
1333 *
1334 * @return array<string, mixed>
1335 */
1336 protected function apply_product_filters(array $args, array $settings): array
1337 {
1338 $filter_args = [];
1339
1340 $include_ids = $this->parse_id_list($settings['kng_include_products'] ?? '');
1341 $exclude_ids = $this->parse_id_list($settings['kng_exclude_products'] ?? '');
1342
1343 $post_in = $include_ids;
1344 $post_in_filter_applied = !empty($include_ids);
1345
1346 if (($settings['kng_on_sale'] ?? '') === 'yes') {
1347 $sale_ids = function_exists('wc_get_product_ids_on_sale') ? wc_get_product_ids_on_sale() : [];
1348 $post_in_filter_applied = true;
1349 $post_in = $post_in ? array_values(array_intersect($post_in, $sale_ids)) : $sale_ids;
1350 }
1351
1352 if ($post_in_filter_applied) {
1353 $filter_args['post__in'] = !empty($post_in) ? $post_in : [0];
1354 }
1355
1356 if (!empty($exclude_ids)) {
1357 $filter_args['post__not_in'] = $exclude_ids;
1358 }
1359
1360 $tax_query = [];
1361 if (($settings['kng_featured_only'] ?? '') === 'yes') {
1362 $tax_query[] = [
1363 'taxonomy' => 'product_visibility',
1364 'field' => 'name',
1365 'terms' => ['featured'],
1366 'operator' => 'IN',
1367 ];
1368 }
1369
1370 if (!empty($tax_query)) {
1371 $filter_args['tax_query'] = $tax_query;
1372 }
1373
1374 $meta_query = [];
1375 if (($settings['kng_in_stock'] ?? '') === 'yes') {
1376 $meta_query[] = [
1377 'key' => '_stock_status',
1378 'value' => 'instock',
1379 ];
1380 }
1381
1382 if (!empty($meta_query)) {
1383 $filter_args['meta_query'] = $meta_query;
1384 }
1385
1386 return $this->merge_query_parts($args, $filter_args);
1387 }
1388
1389 /**
1390 * Parse a comma-separated list of IDs.
1391 *
1392 * @param string $raw Raw input.
1393 *
1394 * @return array<int, int>
1395 */
1396 protected function parse_id_list(string $raw): array
1397 {
1398 if ('' === trim($raw)) {
1399 return [];
1400 }
1401
1402 $ids = array_filter(array_map('absint', array_map('trim', explode(',', $raw))));
1403
1404 return array_values(array_unique($ids));
1405 }
1406
1407 /**
1408 * Merge query arguments, preserving tax/meta queries and combining post__in/not_in.
1409 *
1410 * @param array<string, mixed> $args Base query args.
1411 * @param array<string, mixed> $extra Extra query args.
1412 *
1413 * @return array<string, mixed>
1414 */
1415 protected function merge_query_parts(array $args, array $extra): array
1416 {
1417 $tax_query = $args['tax_query'] ?? [];
1418 $meta_query = $args['meta_query'] ?? [];
1419 $post_in = $args['post__in'] ?? [];
1420 $post_in_filter_applied = array_key_exists('post__in', $args);
1421 $post_not_in = $args['post__not_in'] ?? [];
1422
1423 $extra_tax_query = $extra['tax_query'] ?? [];
1424 $extra_meta_query = $extra['meta_query'] ?? [];
1425 $extra_has_post_in = array_key_exists('post__in', $extra);
1426 $extra_post_in = $extra_has_post_in ? (array) $extra['post__in'] : [];
1427 $extra_post_not_in = $extra['post__not_in'] ?? [];
1428
1429 unset($extra['tax_query'], $extra['meta_query'], $extra['post__in'], $extra['post__not_in']);
1430
1431 $args = array_merge($args, $extra);
1432
1433 if ($extra_has_post_in) {
1434 $post_in_filter_applied = true;
1435 $post_in = !empty($post_in)
1436 ? array_values(array_intersect($post_in, $extra_post_in))
1437 : $extra_post_in;
1438 }
1439
1440 if ($post_in_filter_applied) {
1441 $post_in = array_values(array_unique(array_map('absint', (array) $post_in)));
1442 if (count($post_in) > 1 && in_array(0, $post_in, true)) {
1443 $post_in = array_values(array_diff($post_in, [0]));
1444 }
1445 if (empty($post_in)) {
1446 $post_in = [0];
1447 }
1448 $args['post__in'] = $post_in;
1449 }
1450
1451 $post_not_in = array_merge((array) $post_not_in, (array) $extra_post_not_in);
1452 if (!empty($post_not_in)) {
1453 $post_not_in = array_values(array_unique(array_map('absint', $post_not_in)));
1454 $args['post__not_in'] = $post_not_in;
1455 }
1456
1457 $tax_query = array_merge((array) $tax_query, (array) $extra_tax_query);
1458 if (!empty($tax_query)) {
1459 $args['tax_query'] = $this->normalize_query_relation($tax_query);
1460 }
1461
1462 $meta_query = array_merge((array) $meta_query, (array) $extra_meta_query);
1463 if (!empty($meta_query)) {
1464 $args['meta_query'] = $this->normalize_query_relation($meta_query);
1465 }
1466
1467 return $args;
1468 }
1469
1470 /**
1471 * Ensure relation for tax/meta query arrays.
1472 *
1473 * @param array<int|string, mixed> $query Query array.
1474 *
1475 * @return array<int|string, mixed>
1476 */
1477 protected function normalize_query_relation(array $query): array
1478 {
1479 $conditions = array_filter($query, 'is_array');
1480 if (count($conditions) > 1 && empty($query['relation'])) {
1481 $query['relation'] = 'AND';
1482 }
1483
1484 return $query;
1485 }
1486
1487 /**
1488 * Render card.
1489 *
1490 * @param array<string, mixed> $settings Settings.
1491 * @param \WC_Product $product Product.
1492 *
1493 * @return void
1494 */
1495 protected function render_card(array $settings, \WC_Product $product): void
1496 {
1497 $card_classes = ['king-addons-quick-product-grid__card'];
1498 $animation = $settings['kng_hover_animation'] ?? 'none';
1499 if ($animation !== 'none') {
1500 $card_classes[] = 'is-anim-' . sanitize_html_class((string) $animation);
1501 }
1502
1503 $show_rating = ($settings['kng_show_rating'] ?? 'yes') === 'yes';
1504 $show_price = ($settings['kng_show_price'] ?? 'yes') === 'yes';
1505 $show_excerpt = ($settings['kng_show_excerpt'] ?? 'yes') === 'yes';
1506 $show_button = ($settings['kng_show_add_to_cart'] ?? 'yes') === 'yes';
1507 $show_badge = ($settings['kng_show_badge'] ?? 'yes') === 'yes';
1508 $badge_text = !empty($settings['kng_badge_text']) ? (string) $settings['kng_badge_text'] : esc_html__('Sale', 'king-addons');
1509 $excerpt_length = !empty($settings['kng_excerpt_length']) ? max(1, (int) $settings['kng_excerpt_length']) : 16;
1510
1511 ?>
1512 <div class="king-addons-quick-product-grid__item">
1513 <article class="<?php echo esc_attr(implode(' ', $card_classes)); ?>">
1514 <div class="king-addons-quick-product-grid__media">
1515 <a href="<?php the_permalink(); ?>" class="king-addons-quick-product-grid__link">
1516 <?php echo $this->get_product_image_html($product, $settings); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
1517 </a>
1518 <?php if ($show_badge && $product->is_on_sale()) : ?>
1519 <span class="king-addons-quick-product-grid__badge"><?php echo esc_html($badge_text); ?></span>
1520 <?php endif; ?>
1521 </div>
1522
1523 <div class="king-addons-quick-product-grid__body">
1524 <h3 class="king-addons-quick-product-grid__title">
1525 <a href="<?php the_permalink(); ?>"><?php echo esc_html($product->get_name()); ?></a>
1526 </h3>
1527
1528 <?php if ($show_rating && wc_review_ratings_enabled()) : ?>
1529 <div class="king-addons-quick-product-grid__rating">
1530 <?php echo wc_get_rating_html($product->get_average_rating()); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
1531 </div>
1532 <?php endif; ?>
1533
1534 <?php if ($show_price) : ?>
1535 <div class="king-addons-quick-product-grid__price">
1536 <?php echo $product->get_price_html(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
1537 </div>
1538 <?php endif; ?>
1539
1540 <?php if ($show_excerpt) : ?>
1541 <div class="king-addons-quick-product-grid__excerpt">
1542 <?php echo wp_kses_post(wp_trim_words($product->get_short_description(), $excerpt_length)); ?>
1543 </div>
1544 <?php endif; ?>
1545
1546 <?php if ($show_button) : ?>
1547 <div class="king-addons-quick-product-grid__cta">
1548 <?php echo $this->render_add_to_cart_button($product); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
1549 </div>
1550 <?php endif; ?>
1551 </div>
1552 </article>
1553 </div>
1554 <?php
1555 }
1556
1557 /**
1558 * Get product image HTML with Elementor image size control.
1559 *
1560 * @param \WC_Product $product Product instance.
1561 * @param array<string, mixed> $settings Widget settings.
1562 *
1563 * @return string
1564 */
1565 protected function get_product_image_html(\WC_Product $product, array $settings): string
1566 {
1567 $image_id = $product->get_image_id();
1568
1569 if ($image_id) {
1570 $image_html = Core::getAttachmentImageHTML($settings, 'kng_image_size', (int) $image_id);
1571 if (!empty($image_html)) {
1572 return $image_html;
1573 }
1574 }
1575
1576 $fallback_size = $settings['kng_image_size_size'] ?? 'medium';
1577
1578 return $product->get_image($fallback_size);
1579 }
1580
1581 /**
1582 * Render a controlled Add to Cart button without relying on Woo shortcode.
1583 *
1584 * @param \WC_Product $product Product instance.
1585 *
1586 * @return string
1587 */
1588 protected function render_add_to_cart_button(\WC_Product $product): string
1589 {
1590 if (!$product->is_purchasable() || !$product->is_in_stock()) {
1591 return '<span class="king-addons-quick-product-grid__add-to-cart is-disabled">' . esc_html__('Unavailable', 'king-addons') . '</span>';
1592 }
1593
1594 $classes = [
1595 'king-addons-quick-product-grid__add-to-cart',
1596 'button',
1597 'product_type_' . $product->get_type(),
1598 ];
1599
1600 if ($product->supports('ajax_add_to_cart')) {
1601 $classes[] = 'ajax_add_to_cart';
1602 $classes[] = 'add_to_cart_button';
1603 }
1604
1605 $attributes = [
1606 'href' => $product->add_to_cart_url(),
1607 'data-quantity' => 1,
1608 'data-product_id' => $product->get_id(),
1609 'data-product_sku' => $product->get_sku(),
1610 'rel' => 'nofollow',
1611 'class' => implode(' ', array_filter($classes)),
1612 'aria-label' => wp_strip_all_tags($product->add_to_cart_description()),
1613 ];
1614
1615 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>';
1616 }
1617
1618 /**
1619 * Wrapper classes.
1620 *
1621 * @param array<string, mixed> $settings Settings.
1622 *
1623 * @return array<int, string>
1624 */
1625 protected function get_wrapper_classes(array $settings): array
1626 {
1627 $classes = ['king-addons-quick-product-grid'];
1628 $columns = $settings['kng_columns'] ?? 3;
1629 $classes[] = 'king-addons-quick-product-grid--cols-' . sanitize_html_class((string) $columns);
1630 return $classes;
1631 }
1632
1633 /**
1634 * Wrapper inline style.
1635 *
1636 * @param array<string, mixed> $settings Settings.
1637 *
1638 * @return string
1639 */
1640 protected function get_wrapper_style(array $settings): string
1641 {
1642 $parts = [];
1643 if (isset($settings['kng_grid_gap']['size'])) {
1644 $parts[] = '--kng-quick-product-grid-gap:' . (float) $settings['kng_grid_gap']['size'] . ($settings['kng_grid_gap']['unit'] ?? 'px') . ';';
1645 }
1646
1647 return implode(' ', $parts);
1648 }
1649
1650 /**
1651 * Pro notice.
1652 *
1653 * @return void
1654 */
1655 public function register_pro_notice_controls(): void
1656 {
1657 if (!king_addons_freemius()->can_use_premium_code__premium_only()) {
1658 Core::renderProFeaturesSection($this, '', Controls_Manager::RAW_HTML, 'quick-product-grid', [
1659 'Category filter and higher limits',
1660 'Advanced hover animations and skins',
1661 'Advanced badges and ribbons',
1662 'Advanced CTA layouts',
1663 ]);
1664 }
1665 }
1666 }
1667