PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.49
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.49
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.49, at includes/widgets/Quick_Product_Grid/Quick_Product_Grid.php

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