PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.81
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.81
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 51.1.51 All 36 releases
king-addons / includes / widgets / Loop_Grid / Loop_Grid.php

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

1,581 lines 51.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Loop Grid widget.
4 *
5 * Repeats a Loop Builder item template for every entry a query returns.
6 *
7 * @package King_Addons
8 */
9
10 namespace King_Addons;
11
12 use Elementor\Controls_Manager;
13 use Elementor\Plugin as Elementor_Plugin;
14 use Elementor\Widget_Base;
15 use King_Addons\Loop_Builder\Renderer as Loop_Renderer;
16
17 if (!defined('ABSPATH')) {
18 exit;
19 }
20
21 /**
22 * A card holding a Loop Grid that points back at its own template would
23 * recurse until the request runs out of memory — Renderer guards that.
24 */
25 class Loop_Grid extends Widget_Base
26 {
27 /**
28 * Widget slug.
29 *
30 * @return string
31 */
32 public function get_name(): string
33 {
34 return 'king-addons-loop-grid';
35 }
36
37 /**
38 * Widget title.
39 *
40 * @return string
41 */
42 public function get_title(): string
43 {
44 return esc_html__('Loop Grid', 'king-addons');
45 }
46
47 /**
48 * Widget icon.
49 *
50 * @return string
51 */
52 public function get_icon(): string
53 {
54 return 'king-addons-icon king-addons-loop-grid';
55 }
56
57 /**
58 * Categories.
59 *
60 * @return array<int,string>
61 */
62 public function get_categories(): array
63 {
64 return ['king-addons'];
65 }
66
67 /**
68 * Keywords.
69 *
70 * @return array<int,string>
71 */
72 public function get_keywords(): array
73 {
74 return ['loop', 'grid', 'query', 'posts', 'masonry', 'carousel', 'filter', 'king-addons'];
75 }
76
77 /**
78 * Help URL.
79 *
80 * @return string
81 */
82 public function get_custom_help_url()
83 {
84 return 'mailto:bug@kingaddons.com?subject=Bug Report - King Addons&body=Please describe the issue';
85 }
86
87 /**
88 * Style dependencies.
89 *
90 * @return array<int,string>
91 */
92 public function get_style_depends(): array
93 {
94 return [
95 KING_ADDONS_ASSETS_UNIQUE_KEY . '-swiper-swiper',
96 KING_ADDONS_ASSETS_UNIQUE_KEY . '-loop-grid-style',
97 ];
98 }
99
100 /**
101 * Script dependencies.
102 *
103 * @return array<int,string>
104 */
105 public function get_script_depends(): array
106 {
107 return [
108 KING_ADDONS_ASSETS_UNIQUE_KEY . '-imagesloaded-imagesloaded',
109 KING_ADDONS_ASSETS_UNIQUE_KEY . '-isotope-kng',
110 KING_ADDONS_ASSETS_UNIQUE_KEY . '-swiper-swiper',
111 KING_ADDONS_ASSETS_UNIQUE_KEY . '-loop-grid-script',
112 ];
113 }
114
115 /**
116 * Register controls.
117 *
118 * @return void
119 */
120 protected function register_controls(): void
121 {
122 $this->register_layout_controls();
123 $this->register_query_controls();
124 $this->register_filter_controls();
125 $this->register_carousel_controls();
126 $this->register_pagination_controls();
127 $this->register_style_controls();
128 }
129
130 /**
131 * Layout / template.
132 *
133 * @return void
134 */
135 private function register_layout_controls(): void
136 {
137 $this->start_controls_section(
138 'section_layout',
139 ['label' => esc_html__('Layout', 'king-addons')]
140 );
141
142 $templates = ['' => esc_html__('Select a loop item', 'king-addons')];
143 foreach (Loop_Renderer::get_templates() as $id => $title) {
144 $templates[(string) $id] = $title;
145 }
146
147 $this->add_control(
148 'template_id',
149 [
150 'label' => esc_html__('Loop item', 'king-addons'),
151 'type' => Controls_Manager::SELECT,
152 'options' => $templates,
153 'default' => '',
154 'description' => esc_html__('The card that is repeated for every entry.', 'king-addons'),
155 ]
156 );
157
158 $is_pro = self::can_use_pro();
159
160 $this->add_control(
161 'layout',
162 [
163 'label' => esc_html__('Layout', 'king-addons'),
164 'type' => Controls_Manager::SELECT,
165 'options' => [
166 'grid' => esc_html__('Grid', 'king-addons'),
167 'masonry' => $is_pro
168 ? esc_html__('Masonry', 'king-addons')
169 : sprintf(__('Masonry %s', 'king-addons'), '<i class="eicon-pro-icon"></i>'),
170 'carousel' => $is_pro
171 ? esc_html__('Carousel', 'king-addons')
172 : sprintf(__('Carousel %s', 'king-addons'), '<i class="eicon-pro-icon"></i>'),
173 ],
174 'default' => 'grid',
175 'classes' => $is_pro ? '' : 'king-addons-pro-control',
176 ]
177 );
178
179 if (!$is_pro) {
180 Core::renderUpgradeProNotice($this, Controls_Manager::RAW_HTML, 'loop-grid', 'layout', ['masonry', 'carousel']);
181 }
182
183 $this->add_responsive_control(
184 'columns',
185 [
186 'label' => esc_html__('Columns', 'king-addons'),
187 'type' => Controls_Manager::SELECT,
188 'options' => [
189 '1' => '1',
190 '2' => '2',
191 '3' => '3',
192 '4' => '4',
193 '5' => '5',
194 '6' => '6',
195 ],
196 'default' => '3',
197 'tablet_default' => '2',
198 'mobile_default' => '1',
199 'condition' => ['layout!' => 'carousel'],
200 'selectors' => [
201 '{{WRAPPER}} .king-addons-loop-grid--grid .king-addons-loop-grid__items' => 'grid-template-columns: repeat({{VALUE}}, minmax(0, 1fr));',
202 '{{WRAPPER}} .king-addons-loop-grid' => '--ka-loop-cols: {{VALUE}};',
203 ],
204 ]
205 );
206
207 $this->add_responsive_control(
208 'gap',
209 [
210 'label' => esc_html__('Gap', 'king-addons'),
211 'type' => Controls_Manager::SLIDER,
212 'size_units' => ['px', 'em', 'rem'],
213 'range' => ['px' => ['min' => 0, 'max' => 120]],
214 'default' => ['unit' => 'px', 'size' => 24],
215 'selectors' => [
216 '{{WRAPPER}} .king-addons-loop-grid__items' => 'gap: {{SIZE}}{{UNIT}};',
217 '{{WRAPPER}} .king-addons-loop-grid' => '--ka-loop-gap: {{SIZE}}{{UNIT}};',
218 ],
219 ]
220 );
221
222 $this->add_control(
223 'equal_height',
224 [
225 'label' => esc_html__('Equal height cards', 'king-addons'),
226 'type' => Controls_Manager::SWITCHER,
227 'return_value' => 'yes',
228 'default' => 'yes',
229 'condition' => ['layout' => 'grid'],
230 ]
231 );
232
233 $this->end_controls_section();
234 }
235
236 /**
237 * Query.
238 *
239 * @return void
240 */
241 private function register_query_controls(): void
242 {
243 $this->start_controls_section(
244 'section_query',
245 ['label' => esc_html__('Query', 'king-addons')]
246 );
247
248 $this->add_control(
249 'source',
250 [
251 'label' => esc_html__('Source', 'king-addons'),
252 'type' => Controls_Manager::SELECT,
253 'options' => [
254 'custom' => esc_html__('Custom query', 'king-addons'),
255 'current' => esc_html__('Current query', 'king-addons'),
256 ],
257 'default' => 'custom',
258 ]
259 );
260
261 $this->add_control(
262 'post_type',
263 [
264 'label' => esc_html__('Post type', 'king-addons'),
265 'type' => Controls_Manager::SELECT,
266 'options' => $this->get_post_type_options(),
267 'default' => 'post',
268 'condition' => ['source' => 'custom'],
269 ]
270 );
271
272 $this->add_control(
273 'posts_per_page',
274 [
275 'label' => esc_html__('Posts per page', 'king-addons'),
276 'type' => Controls_Manager::NUMBER,
277 'min' => 1,
278 'max' => 100,
279 'default' => 6,
280 ]
281 );
282
283 $this->add_control(
284 'offset',
285 [
286 'label' => esc_html__('Offset', 'king-addons'),
287 'type' => Controls_Manager::NUMBER,
288 'min' => 0,
289 'default' => 0,
290 'condition' => ['source' => 'custom'],
291 ]
292 );
293
294 $this->add_control(
295 'orderby',
296 [
297 'label' => esc_html__('Order by', 'king-addons'),
298 'type' => Controls_Manager::SELECT,
299 'options' => [
300 'date' => esc_html__('Date', 'king-addons'),
301 'title' => esc_html__('Title', 'king-addons'),
302 'modified' => esc_html__('Last modified', 'king-addons'),
303 'menu_order' => esc_html__('Menu order', 'king-addons'),
304 'comment_count' => esc_html__('Comment count', 'king-addons'),
305 'rand' => esc_html__('Random', 'king-addons'),
306 ],
307 'default' => 'date',
308 'condition' => ['source' => 'custom'],
309 ]
310 );
311
312 $this->add_control(
313 'order',
314 [
315 'label' => esc_html__('Order', 'king-addons'),
316 'type' => Controls_Manager::SELECT,
317 'options' => [
318 'DESC' => esc_html__('Descending', 'king-addons'),
319 'ASC' => esc_html__('Ascending', 'king-addons'),
320 ],
321 'default' => 'DESC',
322 'condition' => ['source' => 'custom'],
323 ]
324 );
325
326 $this->add_control(
327 'taxonomy',
328 [
329 'label' => esc_html__('Taxonomy', 'king-addons'),
330 'type' => Controls_Manager::SELECT,
331 'options' => $this->get_taxonomy_options(),
332 'default' => '',
333 'condition' => ['source' => 'custom'],
334 ]
335 );
336
337 $this->add_control(
338 'terms',
339 [
340 'label' => esc_html__('Terms', 'king-addons'),
341 'type' => Controls_Manager::TEXT,
342 'placeholder' => 'news, updates',
343 'description' => esc_html__('Slugs or IDs, comma-separated.', 'king-addons'),
344 'condition' => [
345 'source' => 'custom',
346 'taxonomy!' => '',
347 ],
348 ]
349 );
350
351 $this->add_control(
352 'authors',
353 [
354 'label' => esc_html__('Authors', 'king-addons'),
355 'type' => Controls_Manager::TEXT,
356 'placeholder' => '1, 2',
357 'condition' => ['source' => 'custom'],
358 ]
359 );
360
361 $this->add_control(
362 'include_ids',
363 [
364 'label' => esc_html__('Include IDs', 'king-addons'),
365 'type' => Controls_Manager::TEXT,
366 'condition' => ['source' => 'custom'],
367 ]
368 );
369
370 $this->add_control(
371 'exclude_ids',
372 [
373 'label' => esc_html__('Exclude IDs', 'king-addons'),
374 'type' => Controls_Manager::TEXT,
375 'condition' => ['source' => 'custom'],
376 ]
377 );
378
379 $this->add_control(
380 'exclude_current',
381 [
382 'label' => esc_html__('Exclude current post', 'king-addons'),
383 'type' => Controls_Manager::SWITCHER,
384 'return_value' => 'yes',
385 ]
386 );
387
388 $this->add_control(
389 'query_id',
390 [
391 'label' => esc_html__('Query ID', 'king-addons'),
392 'type' => Controls_Manager::TEXT,
393 'description' => esc_html__('Optional id for the elementor/query/{id} hook.', 'king-addons'),
394 ]
395 );
396
397 $this->add_control(
398 'nothing_found_text',
399 [
400 'label' => esc_html__('Nothing found text', 'king-addons'),
401 'type' => Controls_Manager::TEXT,
402 'default' => esc_html__('Nothing found.', 'king-addons'),
403 ]
404 );
405
406 $this->end_controls_section();
407 }
408
409 /**
410 * Taxonomy filter bar (Pro).
411 *
412 * @return void
413 */
414 private function register_filter_controls(): void
415 {
416 $is_pro = self::can_use_pro();
417
418 $this->start_controls_section(
419 'section_filter',
420 ['label' => esc_html__('Filters', 'king-addons')]
421 );
422
423 $this->add_control(
424 'filter_enable',
425 [
426 'label' => $is_pro
427 ? esc_html__('Filter bar', 'king-addons')
428 : sprintf(__('Filter bar %s', 'king-addons'), '<i class="eicon-pro-icon"></i>'),
429 'type' => Controls_Manager::SWITCHER,
430 'return_value' => 'yes',
431 'classes' => $is_pro ? '' : 'king-addons-pro-control',
432 ]
433 );
434
435 if (!$is_pro) {
436 Core::renderUpgradeProNotice($this, Controls_Manager::RAW_HTML, 'loop-grid', 'filter_enable', ['yes']);
437 }
438
439 $this->add_control(
440 'filter_taxonomy',
441 [
442 'label' => esc_html__('Taxonomy', 'king-addons'),
443 'type' => Controls_Manager::SELECT,
444 'options' => $this->get_taxonomy_options(),
445 'default' => '',
446 'description' => esc_html__('Leave empty to use the taxonomy from the Query section.', 'king-addons'),
447 'condition' => ['filter_enable' => 'yes'],
448 ]
449 );
450
451 $this->add_control(
452 'filter_terms_mode',
453 [
454 'label' => esc_html__('Terms', 'king-addons'),
455 'type' => Controls_Manager::SELECT,
456 'options' => [
457 'all' => esc_html__('All terms', 'king-addons'),
458 'include' => esc_html__('Include', 'king-addons'),
459 'exclude' => esc_html__('Exclude', 'king-addons'),
460 ],
461 'default' => 'all',
462 'condition' => ['filter_enable' => 'yes'],
463 ]
464 );
465
466 $this->add_control(
467 'filter_terms',
468 [
469 'label' => esc_html__('Term slugs or IDs', 'king-addons'),
470 'type' => Controls_Manager::TEXT,
471 'placeholder' => 'news, updates',
472 'condition' => [
473 'filter_enable' => 'yes',
474 'filter_terms_mode!' => 'all',
475 ],
476 ]
477 );
478
479 $this->add_control(
480 'filter_all_label',
481 [
482 'label' => esc_html__('“All” label', 'king-addons'),
483 'type' => Controls_Manager::TEXT,
484 'default' => esc_html__('All', 'king-addons'),
485 'condition' => ['filter_enable' => 'yes'],
486 ]
487 );
488
489 $this->add_control(
490 'filter_deeplink',
491 [
492 'label' => esc_html__('Remember filter in the URL', 'king-addons'),
493 'type' => Controls_Manager::SWITCHER,
494 'return_value' => 'yes',
495 'condition' => ['filter_enable' => 'yes'],
496 ]
497 );
498
499 $this->end_controls_section();
500 }
501
502 /**
503 * Carousel controls (Pro).
504 *
505 * @return void
506 */
507 private function register_carousel_controls(): void
508 {
509 $this->start_controls_section(
510 'section_carousel',
511 [
512 'label' => esc_html__('Carousel', 'king-addons'),
513 'condition' => ['layout' => 'carousel'],
514 ]
515 );
516
517 $this->add_responsive_control(
518 'carousel_slides',
519 [
520 'label' => esc_html__('Slides per view', 'king-addons'),
521 'type' => Controls_Manager::NUMBER,
522 'min' => 1,
523 'max' => 8,
524 'default' => 3,
525 'tablet_default' => 2,
526 'mobile_default' => 1,
527 ]
528 );
529
530 $this->add_control(
531 'carousel_autoplay',
532 [
533 'label' => esc_html__('Autoplay', 'king-addons'),
534 'type' => Controls_Manager::SWITCHER,
535 'return_value' => 'yes',
536 ]
537 );
538
539 $this->add_control(
540 'carousel_delay',
541 [
542 'label' => esc_html__('Autoplay delay (ms)', 'king-addons'),
543 'type' => Controls_Manager::NUMBER,
544 'min' => 500,
545 'default' => 4000,
546 'condition' => ['carousel_autoplay' => 'yes'],
547 ]
548 );
549
550 $this->add_control(
551 'carousel_loop',
552 [
553 'label' => esc_html__('Loop', 'king-addons'),
554 'type' => Controls_Manager::SWITCHER,
555 'return_value' => 'yes',
556 'default' => 'yes',
557 ]
558 );
559
560 $this->add_control(
561 'carousel_arrows',
562 [
563 'label' => esc_html__('Arrows', 'king-addons'),
564 'type' => Controls_Manager::SWITCHER,
565 'return_value' => 'yes',
566 'default' => 'yes',
567 ]
568 );
569
570 $this->add_control(
571 'carousel_dots',
572 [
573 'label' => esc_html__('Dots', 'king-addons'),
574 'type' => Controls_Manager::SWITCHER,
575 'return_value' => 'yes',
576 'default' => 'yes',
577 ]
578 );
579
580 $this->add_control(
581 'carousel_speed',
582 [
583 'label' => esc_html__('Speed (ms)', 'king-addons'),
584 'type' => Controls_Manager::NUMBER,
585 'min' => 100,
586 'default' => 400,
587 ]
588 );
589
590 $this->end_controls_section();
591 }
592
593 /**
594 * Pagination.
595 *
596 * @return void
597 */
598 private function register_pagination_controls(): void
599 {
600 $this->start_controls_section(
601 'section_pagination',
602 [
603 'label' => esc_html__('Pagination', 'king-addons'),
604 'condition' => ['layout!' => 'carousel'],
605 ]
606 );
607
608 $this->add_control(
609 'pagination_type',
610 [
611 'label' => esc_html__('Pagination', 'king-addons'),
612 'type' => Controls_Manager::SELECT,
613 'options' => [
614 'none' => esc_html__('None', 'king-addons'),
615 'numbers' => esc_html__('Numbers', 'king-addons'),
616 'load_more' => esc_html__('Load more', 'king-addons'),
617 ],
618 'default' => 'none',
619 'condition' => ['layout!' => 'carousel'],
620 ]
621 );
622
623 $this->add_control(
624 'load_more_text',
625 [
626 'label' => esc_html__('Load more text', 'king-addons'),
627 'type' => Controls_Manager::TEXT,
628 'default' => esc_html__('Load more', 'king-addons'),
629 'condition' => ['pagination_type' => 'load_more'],
630 ]
631 );
632
633 $this->add_control(
634 'all_loaded_text',
635 [
636 'label' => esc_html__('All loaded text', 'king-addons'),
637 'type' => Controls_Manager::TEXT,
638 'default' => esc_html__('All items loaded', 'king-addons'),
639 'condition' => ['pagination_type' => 'load_more'],
640 ]
641 );
642
643 $this->end_controls_section();
644 }
645
646 /**
647 * Style controls.
648 *
649 * @return void
650 */
651 private function register_style_controls(): void
652 {
653 $this->start_controls_section(
654 'section_style_items',
655 [
656 'label' => esc_html__('Items', 'king-addons'),
657 'tab' => Controls_Manager::TAB_STYLE,
658 ]
659 );
660
661 $this->add_control(
662 'item_background',
663 [
664 'label' => esc_html__('Background', 'king-addons'),
665 'type' => Controls_Manager::COLOR,
666 'selectors' => [
667 '{{WRAPPER}} .king-addons-loop-grid__item' => 'background-color: {{VALUE}};',
668 ],
669 ]
670 );
671
672 $this->add_responsive_control(
673 'item_padding',
674 [
675 'label' => esc_html__('Padding', 'king-addons'),
676 'type' => Controls_Manager::DIMENSIONS,
677 'size_units' => ['px', 'em', '%'],
678 'selectors' => [
679 '{{WRAPPER}} .king-addons-loop-grid__item' => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
680 ],
681 ]
682 );
683
684 $this->add_responsive_control(
685 'item_radius',
686 [
687 'label' => esc_html__('Border radius', 'king-addons'),
688 'type' => Controls_Manager::DIMENSIONS,
689 'size_units' => ['px', '%'],
690 'selectors' => [
691 '{{WRAPPER}} .king-addons-loop-grid__item' => 'border-radius: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}}; overflow: hidden;',
692 ],
693 ]
694 );
695
696 $this->end_controls_section();
697
698 $this->start_controls_section(
699 'section_style_pagination',
700 [
701 'label' => esc_html__('Pagination', 'king-addons'),
702 'tab' => Controls_Manager::TAB_STYLE,
703 ]
704 );
705
706 $this->add_responsive_control(
707 'pagination_spacing',
708 [
709 'label' => esc_html__('Spacing', 'king-addons'),
710 'type' => Controls_Manager::SLIDER,
711 'size_units' => ['px', 'em'],
712 'range' => ['px' => ['min' => 0, 'max' => 120]],
713 'selectors' => [
714 '{{WRAPPER}} .king-addons-loop-grid__footer' => 'margin-top: {{SIZE}}{{UNIT}};',
715 ],
716 ]
717 );
718
719 $this->add_control(
720 'pagination_color',
721 [
722 'label' => esc_html__('Color', 'king-addons'),
723 'type' => Controls_Manager::COLOR,
724 'selectors' => [
725 '{{WRAPPER}} .king-addons-loop-grid__pagination' => 'color: {{VALUE}};',
726 '{{WRAPPER}} .king-addons-loop-grid__load-more' => 'color: {{VALUE}};',
727 '{{WRAPPER}} .king-addons-loop-grid__all-loaded' => 'color: {{VALUE}};',
728 ],
729 ]
730 );
731
732 $this->add_control(
733 'pagination_button_background',
734 [
735 'label' => esc_html__('Button background', 'king-addons'),
736 'type' => Controls_Manager::COLOR,
737 'selectors' => [
738 '{{WRAPPER}} .king-addons-loop-grid__pagination .page-numbers' => 'background-color: {{VALUE}};',
739 '{{WRAPPER}} .king-addons-loop-grid__load-more' => 'background-color: {{VALUE}};',
740 ],
741 ]
742 );
743
744 $this->add_control(
745 'empty_color',
746 [
747 'label' => esc_html__('Empty text color', 'king-addons'),
748 'type' => Controls_Manager::COLOR,
749 'selectors' => [
750 '{{WRAPPER}} .king-addons-loop-grid__empty' => 'color: {{VALUE}};',
751 ],
752 ]
753 );
754
755 $this->end_controls_section();
756 }
757
758 /**
759 * Render the grid.
760 *
761 * @return void
762 */
763 protected function render(): void
764 {
765 $settings = $this->get_settings_for_display();
766 $template_id = absint($settings['template_id'] ?? 0);
767
768 if (!Loop_Renderer::is_template($template_id)) {
769 $this->print_notice(esc_html__('Pick a loop item in the Layout section.', 'king-addons'));
770 return;
771 }
772
773 $page = self::resolve_page_number($this->get_id());
774 $filter = self::resolve_filter_term($this->get_id(), $settings);
775 $query = self::build_query($settings, $page, self::current_context(), $filter);
776
777 echo self::render_grid($settings, $query, $template_id, $this->get_id(), $page, self::current_document_id(), $filter); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
778 }
779
780 /**
781 * Editor / front notice.
782 *
783 * @param string $message Message.
784 *
785 * @return void
786 */
787 private function print_notice(string $message): void
788 {
789 echo '<div class="king-addons-loop-grid__notice">' . esc_html($message) . '</div>';
790 }
791
792 /**
793 * @param array<string,mixed> $settings Widget settings.
794 * @param \WP_Query $query Query.
795 * @param int $template_id Loop item template.
796 * @param string $element_id Element id.
797 * @param int $page Page number.
798 * @param int $document_id Document the widget belongs to.
799 * @param string $filter Active filter term slug or ID.
800 *
801 * @return string
802 */
803 private static function render_grid(array $settings, \WP_Query $query, int $template_id, string $element_id, int $page, int $document_id, string $filter = ''): string
804 {
805 $layout = self::resolve_layout($settings);
806 $pagination = (string) ($settings['pagination_type'] ?? 'none');
807 if ('carousel' === $layout || !in_array($pagination, ['none', 'numbers', 'load_more'], true)) {
808 $pagination = 'none';
809 }
810
811 ob_start();
812
813 $classes = ['king-addons-loop-grid', 'king-addons-loop-grid--' . $layout];
814 if ('yes' === ($settings['equal_height'] ?? '') && 'grid' === $layout) {
815 $classes[] = 'king-addons-loop-grid--equal';
816 }
817
818 $context = self::current_context();
819 $swiper = self::carousel_config($settings);
820 ?>
821 <div class="<?php echo esc_attr(implode(' ', $classes)); ?>"
822 data-ka-loop-grid="1"
823 data-layout="<?php echo esc_attr($layout); ?>"
824 data-element-id="<?php echo esc_attr($element_id); ?>"
825 data-post-id="<?php echo esc_attr((string) $document_id); ?>"
826 data-page="<?php echo esc_attr((string) $page); ?>"
827 data-max-pages="<?php echo esc_attr((string) max(1, (int) $query->max_num_pages)); ?>"
828 data-context="<?php echo esc_attr((string) wp_json_encode($context)); ?>"
829 data-filter="<?php echo esc_attr($filter); ?>"
830 data-swiper="<?php echo esc_attr((string) wp_json_encode($swiper)); ?>"
831 data-ajax-url="<?php echo esc_url(admin_url('admin-ajax.php')); ?>"
832 data-nonce="<?php echo esc_attr(wp_create_nonce('ka_loop_grid')); ?>">
833 <?php echo self::render_filter_bar($settings, $element_id, $filter); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
834 <?php if (!$query->have_posts()) : ?>
835 <div class="king-addons-loop-grid__empty"><?php echo esc_html((string) ($settings['nothing_found_text'] ?? '')); ?></div>
836 <?php else : ?>
837 <?php if ('carousel' === $layout) : ?>
838 <div class="swiper king-addons-loop-grid__swiper">
839 <div class="swiper-wrapper king-addons-loop-grid__items">
840 <?php echo self::render_items($query, $template_id, $layout); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
841 </div>
842 <?php if (!empty($swiper['arrows'])) : ?>
843 <button type="button" class="swiper-button-prev" aria-label="<?php echo esc_attr__('Previous', 'king-addons'); ?>"></button>
844 <button type="button" class="swiper-button-next" aria-label="<?php echo esc_attr__('Next', 'king-addons'); ?>"></button>
845 <?php endif; ?>
846 <?php if (!empty($swiper['dots'])) : ?>
847 <div class="swiper-pagination"></div>
848 <?php endif; ?>
849 </div>
850 <?php else : ?>
851 <div class="king-addons-loop-grid__items">
852 <?php if ('masonry' === $layout) : ?>
853 <div class="king-addons-loop-grid__sizer"></div>
854 <?php endif; ?>
855 <?php echo self::render_items($query, $template_id, $layout); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
856 </div>
857 <?php endif; ?>
858
859 <?php if ('none' !== $pagination && (int) $query->max_num_pages > 1) : ?>
860 <div class="king-addons-loop-grid__footer">
861 <?php echo self::render_pagination($settings, $query, $element_id, $page, $pagination); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
862 </div>
863 <?php endif; ?>
864 <?php endif; ?>
865 </div>
866 <?php
867
868 return (string) ob_get_clean();
869 }
870
871 /**
872 * @param \WP_Query $query Query to walk.
873 * @param int $template_id Loop item template.
874 * @param string $layout grid|masonry|carousel.
875 *
876 * @return string
877 */
878 private static function render_items(\WP_Query $query, int $template_id, string $layout = 'grid'): string
879 {
880 $html = '';
881 $item_class = 'carousel' === $layout
882 ? 'swiper-slide king-addons-loop-grid__item'
883 : 'king-addons-loop-grid__item';
884
885 foreach ($query->posts as $post) {
886 $post_id = $post instanceof \WP_Post ? (int) $post->ID : (int) $post;
887
888 $html .= '<div class="' . esc_attr($item_class) . '" data-post-id="' . esc_attr((string) $post_id) . '">';
889 $html .= Loop_Renderer::render($template_id, $post_id);
890 $html .= '</div>';
891 }
892
893 return $html;
894 }
895
896 /**
897 * Numbers or load-more markup.
898 *
899 * @param array<string,mixed> $settings Widget settings.
900 * @param \WP_Query $query Query.
901 * @param string $element_id Element id.
902 * @param int $page Current page.
903 * @param string $pagination numbers|load_more.
904 *
905 * @return string
906 */
907 private static function render_pagination(array $settings, \WP_Query $query, string $element_id, int $page, string $pagination): string
908 {
909 $max = max(1, (int) $query->max_num_pages);
910
911 if ('load_more' === $pagination) {
912 $loaded = $page >= $max;
913 $html = '';
914 if (!$loaded) {
915 $label = (string) ($settings['load_more_text'] ?? __('Load more', 'king-addons'));
916 $html .= '<button type="button" class="king-addons-loop-grid__load-more">' . esc_html($label) . '</button>';
917 }
918 $done = (string) ($settings['all_loaded_text'] ?? __('All items loaded', 'king-addons'));
919 $html .= '<div class="king-addons-loop-grid__all-loaded"' . ($loaded ? '' : ' hidden') . '>' . esc_html($done) . '</div>';
920
921 return $html;
922 }
923
924 $arg = 'ka-loop-' . preg_replace('/[^a-zA-Z0-9_-]/', '', $element_id);
925 $links = paginate_links([
926 'base' => esc_url_raw(add_query_arg($arg, '%#%', false) ?: ''),
927 'format' => '',
928 'current' => max(1, $page),
929 'total' => $max,
930 'type' => 'plain',
931 'prev_next' => true,
932 ]);
933
934 if (!is_string($links) || '' === $links) {
935 return '';
936 }
937
938 return '<nav class="king-addons-loop-grid__pagination">' . $links . '</nav>';
939 }
940
941 /**
942 * @param array<string,mixed> $settings Widget settings.
943 * @param int $page Page number.
944 * @param array<string,mixed> $context Archive context.
945 * @param string $filter Active filter term.
946 *
947 * @return \WP_Query
948 */
949 private static function build_query(array $settings, int $page, array $context, string $filter = ''): \WP_Query
950 {
951 $per_page = max(1, (int) ($settings['posts_per_page'] ?? 6));
952 $source = (string) ($settings['source'] ?? 'custom');
953
954 if ('current' === $source) {
955 global $wp_query;
956 $main = [];
957
958 if (!wp_doing_ajax() && $wp_query instanceof \WP_Query) {
959 $main = $wp_query->query_vars;
960 unset($main['pagename'], $main['page_id'], $main['name'], $main['p'], $main['error']);
961 } else {
962 $main = self::context_to_query_vars($context);
963 }
964
965 return new \WP_Query(self::apply_filter_args(array_merge($main, [
966 'posts_per_page' => $per_page,
967 'paged' => max(1, $page),
968 'ignore_sticky_posts' => true,
969 'no_found_rows' => false,
970 ]), $settings, $filter));
971 }
972
973 $post_type = sanitize_key((string) ($settings['post_type'] ?? 'post'));
974 if ('' === $post_type || !post_type_exists($post_type)) {
975 $post_type = 'post';
976 }
977
978 $orderby = (string) ($settings['orderby'] ?? 'date');
979 if (!in_array($orderby, ['date', 'title', 'modified', 'menu_order', 'comment_count', 'rand'], true)) {
980 $orderby = 'date';
981 }
982
983 $order = strtoupper((string) ($settings['order'] ?? 'DESC'));
984 $order = 'ASC' === $order ? 'ASC' : 'DESC';
985
986 $args = [
987 'post_type' => $post_type,
988 'post_status' => 'publish',
989 'posts_per_page' => $per_page,
990 'paged' => max(1, $page),
991 'orderby' => $orderby,
992 'order' => $order,
993 'ignore_sticky_posts' => true,
994 'no_found_rows' => false,
995 ];
996
997 $offset = absint($settings['offset'] ?? 0);
998 if ($offset > 0) {
999 $args['offset'] = $offset + ((max(1, $page) - 1) * $per_page);
1000 unset($args['paged']);
1001 }
1002
1003 $include = self::id_list((string) ($settings['include_ids'] ?? ''));
1004 if (!empty($include)) {
1005 $args['post__in'] = $include;
1006 $args['posts_per_page'] = max($per_page, count($include));
1007 }
1008
1009 $exclude = self::id_list((string) ($settings['exclude_ids'] ?? ''));
1010 if ('yes' === ($settings['exclude_current'] ?? '')) {
1011 $current = (int) get_the_ID();
1012 if ($current > 0) {
1013 $exclude[] = $current;
1014 }
1015 }
1016 if (!empty($exclude)) {
1017 $args['post__not_in'] = array_values(array_unique($exclude));
1018 }
1019
1020 $authors = self::id_list((string) ($settings['authors'] ?? ''));
1021 if (!empty($authors)) {
1022 $args['author__in'] = $authors;
1023 }
1024
1025 $taxonomy = sanitize_key((string) ($settings['taxonomy'] ?? ''));
1026 $terms = self::split_list((string) ($settings['terms'] ?? ''));
1027 if ('' !== $taxonomy && taxonomy_exists($taxonomy) && !empty($terms)) {
1028 $numeric = array_filter($terms, 'is_numeric');
1029 $args['tax_query'] = [
1030 [
1031 'taxonomy' => $taxonomy,
1032 'field' => count($numeric) === count($terms) ? 'term_id' : 'slug',
1033 'terms' => count($numeric) === count($terms) ? array_map('intval', $terms) : $terms,
1034 ],
1035 ];
1036 }
1037
1038 $query_id = sanitize_key((string) ($settings['query_id'] ?? ''));
1039 if ('' !== $query_id) {
1040 $args = apply_filters('elementor/query/' . $query_id, $args, $settings);
1041 if (!is_array($args)) {
1042 $args = [];
1043 }
1044 }
1045
1046 return new \WP_Query(self::apply_filter_args($args, $settings, $filter));
1047 }
1048
1049 /**
1050 * Load-more / paging for the Loop Grid widget.
1051 *
1052 * @return void
1053 */
1054 public static function handle_ajax(): void
1055 {
1056 check_ajax_referer('ka_loop_grid', 'nonce');
1057
1058 $document_id = absint($_POST['post_id'] ?? 0);
1059 $element_id = sanitize_text_field(wp_unslash($_POST['element_id'] ?? ''));
1060 $page = absint($_POST['page'] ?? 1);
1061 $context_raw = wp_unslash($_POST['context'] ?? '{}');
1062 $context = json_decode(is_string($context_raw) ? $context_raw : '{}', true);
1063 if (!is_array($context)) {
1064 $context = [];
1065 }
1066
1067 if ($document_id < 1 || '' === $element_id || !class_exists('Elementor\\Plugin')) {
1068 wp_send_json_error(['message' => 'invalid'], 400);
1069 }
1070
1071 $document = Elementor_Plugin::$instance->documents->get($document_id);
1072 if (!$document) {
1073 wp_send_json_error(['message' => 'missing'], 404);
1074 }
1075
1076 $settings = self::find_widget_settings($document, $element_id);
1077 if (empty($settings)) {
1078 wp_send_json_error(['message' => 'missing'], 404);
1079 }
1080
1081 $template_id = absint($settings['template_id'] ?? 0);
1082 if (!Loop_Renderer::is_template($template_id)) {
1083 wp_send_json_error(['message' => 'template'], 400);
1084 }
1085
1086 $filter = isset($_POST['filter']) ? sanitize_text_field(wp_unslash($_POST['filter'])) : '';
1087 if (!self::filters_enabled($settings)) {
1088 $filter = '';
1089 }
1090
1091 $layout = self::resolve_layout($settings);
1092 $query = self::build_query($settings, max(1, $page), $context, $filter);
1093
1094 // Each response is a fresh document as far as the browser is concerned.
1095 Loop_Renderer::reset_css_state();
1096
1097 $pagination = (string) ($settings['pagination_type'] ?? 'none');
1098 if ('carousel' === $layout) {
1099 $pagination = 'none';
1100 }
1101
1102 wp_send_json_success([
1103 'html' => self::render_items($query, $template_id, $layout),
1104 'page' => max(1, $page),
1105 'max_pages' => max(1, (int) $query->max_num_pages),
1106 'empty' => !$query->have_posts(),
1107 'empty_text' => (string) ($settings['nothing_found_text'] ?? ''),
1108 'pagination' => ('none' !== $pagination && $query->have_posts())
1109 ? self::render_pagination($settings, $query, $element_id, max(1, $page), $pagination)
1110 : '',
1111 ]);
1112 }
1113
1114 /**
1115 * Settings for one widget inside a document.
1116 *
1117 * @param \Elementor\Core\Base\Document $document Document.
1118 * @param string $element_id Element id.
1119 *
1120 * @return array<string,mixed>
1121 */
1122 private static function find_widget_settings($document, string $element_id): array
1123 {
1124 $found = [];
1125
1126 $walk = static function ($els) use (&$walk, &$found, $element_id) {
1127 foreach ((array) $els as $el) {
1128 if (($el['id'] ?? '') === $element_id) {
1129 $found = is_array($el['settings'] ?? null) ? $el['settings'] : [];
1130 return;
1131 }
1132 if (!empty($el['elements'])) {
1133 $walk($el['elements']);
1134 }
1135 }
1136 };
1137
1138 $walk((array) $document->get_elements_data());
1139
1140 return $found;
1141 }
1142
1143 /**
1144 * Whether the current site can use Loop Grid Pro layouts.
1145 *
1146 * @return bool
1147 */
1148 private static function can_use_pro(): bool
1149 {
1150 return function_exists('king_addons_can_use_pro') && king_addons_can_use_pro();
1151 }
1152
1153 /**
1154 * Layout that will actually render.
1155 *
1156 * @param array<string,mixed> $settings Widget settings.
1157 *
1158 * @return string
1159 */
1160 private static function resolve_layout(array $settings): string
1161 {
1162 $layout = (string) ($settings['layout'] ?? 'grid');
1163 if (!in_array($layout, ['grid', 'masonry', 'carousel'], true)) {
1164 $layout = 'grid';
1165 }
1166
1167 if ('grid' !== $layout && !self::can_use_pro()) {
1168 return 'grid';
1169 }
1170
1171 return $layout;
1172 }
1173
1174 /**
1175 * Whether the filter bar is on and allowed.
1176 *
1177 * @param array<string,mixed> $settings Widget settings.
1178 *
1179 * @return bool
1180 */
1181 private static function filters_enabled(array $settings): bool
1182 {
1183 return self::can_use_pro() && 'yes' === ($settings['filter_enable'] ?? '');
1184 }
1185
1186 /**
1187 * Taxonomy used by the filter bar.
1188 *
1189 * @param array<string,mixed> $settings Widget settings.
1190 *
1191 * @return string
1192 */
1193 private static function filter_taxonomy(array $settings): string
1194 {
1195 $taxonomy = sanitize_key((string) ($settings['filter_taxonomy'] ?? ''));
1196 if ('' === $taxonomy) {
1197 $taxonomy = sanitize_key((string) ($settings['taxonomy'] ?? ''));
1198 }
1199
1200 return taxonomy_exists($taxonomy) ? $taxonomy : '';
1201 }
1202
1203 /**
1204 * Active filter from the URL, if deeplinks are on.
1205 *
1206 * @param string $element_id Element id.
1207 * @param array<string,mixed> $settings Widget settings.
1208 *
1209 * @return string
1210 */
1211 private static function resolve_filter_term(string $element_id, array $settings): string
1212 {
1213 if (!self::filters_enabled($settings) || 'yes' !== ($settings['filter_deeplink'] ?? '')) {
1214 return '';
1215 }
1216
1217 $arg = 'ka-lf-' . preg_replace('/[^a-zA-Z0-9_-]/', '', $element_id);
1218 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1219 if (!isset($_GET[$arg])) {
1220 return '';
1221 }
1222
1223 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1224 return sanitize_text_field(wp_unslash($_GET[$arg]));
1225 }
1226
1227 /**
1228 * Restrict a query to one filter term.
1229 *
1230 * @param array<string,mixed> $args WP_Query args.
1231 * @param array<string,mixed> $settings Widget settings.
1232 * @param string $filter Term slug or ID.
1233 *
1234 * @return array<string,mixed>
1235 */
1236 private static function apply_filter_args(array $args, array $settings, string $filter): array
1237 {
1238 $filter = trim($filter);
1239 if ('' === $filter || !self::filters_enabled($settings)) {
1240 return $args;
1241 }
1242
1243 $taxonomy = self::filter_taxonomy($settings);
1244 if ('' === $taxonomy) {
1245 return $args;
1246 }
1247
1248 $field = is_numeric($filter) ? 'term_id' : 'slug';
1249 $term = is_numeric($filter) ? (int) $filter : $filter;
1250 $clause = [
1251 'taxonomy' => $taxonomy,
1252 'field' => $field,
1253 'terms' => [$term],
1254 ];
1255
1256 if (empty($args['tax_query']) || !is_array($args['tax_query'])) {
1257 $args['tax_query'] = [$clause];
1258 return $args;
1259 }
1260
1261 $args['tax_query']['relation'] = 'AND';
1262 $args['tax_query'][] = $clause;
1263
1264 return $args;
1265 }
1266
1267 /**
1268 * Swiper config written onto the grid for the front-end script.
1269 *
1270 * @param array<string,mixed> $settings Widget settings.
1271 *
1272 * @return array<string,mixed>
1273 */
1274 private static function carousel_config(array $settings): array
1275 {
1276 $slides = $settings['carousel_slides'] ?? 3;
1277 if (is_array($slides)) {
1278 $slides = $slides['size'] ?? 3;
1279 }
1280
1281 $tablet = $settings['carousel_slides_tablet'] ?? 2;
1282 if (is_array($tablet)) {
1283 $tablet = $tablet['size'] ?? 2;
1284 }
1285
1286 $mobile = $settings['carousel_slides_mobile'] ?? 1;
1287 if (is_array($mobile)) {
1288 $mobile = $mobile['size'] ?? 1;
1289 }
1290
1291 $gap = $settings['gap'] ?? ['size' => 24];
1292 $gap_size = is_array($gap) ? (float) ($gap['size'] ?? 24) : (float) $gap;
1293
1294 return [
1295 'slides' => max(1, (int) $slides),
1296 'slidesTablet' => max(1, (int) $tablet),
1297 'slidesMobile' => max(1, (int) $mobile),
1298 'gap' => $gap_size,
1299 'autoplay' => 'yes' === ($settings['carousel_autoplay'] ?? ''),
1300 'delay' => max(500, (int) ($settings['carousel_delay'] ?? 4000)),
1301 'loop' => 'yes' === ($settings['carousel_loop'] ?? 'yes'),
1302 'arrows' => 'yes' === ($settings['carousel_arrows'] ?? 'yes'),
1303 'dots' => 'yes' === ($settings['carousel_dots'] ?? 'yes'),
1304 'speed' => max(100, (int) ($settings['carousel_speed'] ?? 400)),
1305 ];
1306 }
1307
1308 /**
1309 * Filter bar markup.
1310 *
1311 * @param array<string,mixed> $settings Widget settings.
1312 * @param string $element_id Element id.
1313 * @param string $active Active term.
1314 *
1315 * @return string
1316 */
1317 private static function render_filter_bar(array $settings, string $element_id, string $active): string
1318 {
1319 if (!self::filters_enabled($settings)) {
1320 return '';
1321 }
1322
1323 $taxonomy = self::filter_taxonomy($settings);
1324 if ('' === $taxonomy) {
1325 return '';
1326 }
1327
1328 $terms = get_terms([
1329 'taxonomy' => $taxonomy,
1330 'hide_empty' => true,
1331 ]);
1332
1333 if (is_wp_error($terms) || empty($terms)) {
1334 return '';
1335 }
1336
1337 $mode = (string) ($settings['filter_terms_mode'] ?? 'all');
1338 $list = self::split_list((string) ($settings['filter_terms'] ?? ''));
1339 if ('include' === $mode && !empty($list)) {
1340 $terms = array_values(array_filter($terms, static function ($term) use ($list) {
1341 return in_array((string) $term->term_id, $list, true) || in_array($term->slug, $list, true);
1342 }));
1343 } elseif ('exclude' === $mode && !empty($list)) {
1344 $terms = array_values(array_filter($terms, static function ($term) use ($list) {
1345 return !in_array((string) $term->term_id, $list, true) && !in_array($term->slug, $list, true);
1346 }));
1347 }
1348
1349 if (empty($terms)) {
1350 return '';
1351 }
1352
1353 $all_label = (string) ($settings['filter_all_label'] ?? __('All', 'king-addons'));
1354 $deeplink = 'yes' === ($settings['filter_deeplink'] ?? '') ? '1' : '';
1355 $html = '<div class="king-addons-loop-grid__filters" role="toolbar" data-deeplink="' . esc_attr($deeplink) . '">';
1356 $html .= self::filter_button($all_label, '', '' === $active);
1357 foreach ($terms as $term) {
1358 $html .= self::filter_button($term->name, $term->slug, $active === $term->slug || $active === (string) $term->term_id);
1359 }
1360 $html .= '</div>';
1361
1362 return $html;
1363 }
1364
1365 /**
1366 * One filter button.
1367 *
1368 * @param string $label Label.
1369 * @param string $value Term slug, empty for All.
1370 * @param bool $active Whether this button is pressed.
1371 *
1372 * @return string
1373 */
1374 private static function filter_button(string $label, string $value, bool $active): string
1375 {
1376 return sprintf(
1377 '<button type="button" class="king-addons-loop-grid__filter%1$s" data-filter="%2$s" aria-pressed="%3$s">%4$s</button>',
1378 $active ? ' is-active' : '',
1379 esc_attr($value),
1380 $active ? 'true' : 'false',
1381 esc_html($label)
1382 );
1383 }
1384
1385 /**
1386 * Page from the URL, if this grid paginates with numbers.
1387 *
1388 * @param string $element_id Element id.
1389 *
1390 * @return int
1391 */
1392 private static function resolve_page_number(string $element_id): int
1393 {
1394 $arg = 'ka-loop-' . preg_replace('/[^a-zA-Z0-9_-]/', '', $element_id);
1395 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1396 if (isset($_GET[$arg])) {
1397 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1398 return max(1, absint(wp_unslash($_GET[$arg])));
1399 }
1400
1401 return 1;
1402 }
1403
1404 /**
1405 * Archive / search flags written onto the grid for AJAX.
1406 *
1407 * @return array<string,mixed>
1408 */
1409 private static function current_context(): array
1410 {
1411 $object = get_queried_object();
1412 $post_type = get_query_var('post_type');
1413 if (is_array($post_type)) {
1414 $post_type = implode(',', array_map('sanitize_key', $post_type));
1415 }
1416
1417 $context = [
1418 'is_search' => is_search(),
1419 's' => is_search() ? (string) get_query_var('s') : '',
1420 'is_home' => is_home(),
1421 'is_post_type_archive' => is_post_type_archive(),
1422 'post_type' => is_string($post_type) ? $post_type : '',
1423 'taxonomy' => '',
1424 'term' => '',
1425 'author' => is_author() ? (int) get_query_var('author') : 0,
1426 ];
1427
1428 if ($object instanceof \WP_Term) {
1429 $context['taxonomy'] = $object->taxonomy;
1430 $context['term'] = $object->slug;
1431 }
1432
1433 return $context;
1434 }
1435
1436 /**
1437 * Rebuild query vars from a stored archive context.
1438 *
1439 * @param array<string,mixed> $context Context.
1440 *
1441 * @return array<string,mixed>
1442 */
1443 private static function context_to_query_vars(array $context): array
1444 {
1445 $args = [
1446 'post_status' => 'publish',
1447 'ignore_sticky_posts' => true,
1448 ];
1449
1450 if (!empty($context['is_search'])) {
1451 $args['s'] = (string) ($context['s'] ?? '');
1452 $args['post_type'] = 'any';
1453 return $args;
1454 }
1455
1456 if (!empty($context['taxonomy']) && !empty($context['term'])) {
1457 $args['tax_query'] = [
1458 [
1459 'taxonomy' => sanitize_key((string) $context['taxonomy']),
1460 'field' => 'slug',
1461 'terms' => [(string) $context['term']],
1462 ],
1463 ];
1464 $args['post_type'] = 'any';
1465 return $args;
1466 }
1467
1468 if (!empty($context['author'])) {
1469 $args['author'] = absint($context['author']);
1470 }
1471
1472 $post_type = sanitize_key((string) ($context['post_type'] ?? ''));
1473 if ('' !== $post_type && post_type_exists($post_type)) {
1474 $args['post_type'] = $post_type;
1475 }
1476
1477 return $args;
1478 }
1479
1480 /**
1481 * Document the widget is rendering in.
1482 *
1483 * @return int
1484 */
1485 private static function current_document_id(): int
1486 {
1487 if (class_exists('Elementor\\Plugin')) {
1488 $document = Elementor_Plugin::$instance->documents->get_current();
1489 if ($document) {
1490 return (int) $document->get_main_id();
1491 }
1492 }
1493
1494 return (int) get_the_ID();
1495 }
1496
1497 /**
1498 * Post types a custom query can target.
1499 *
1500 * @return array<string,string>
1501 */
1502 private function get_post_type_options(): array
1503 {
1504 $skip = [
1505 'attachment',
1506 'elementor_library',
1507 'e-floating-buttons',
1508 'king-addons-el-hf',
1509 'king_addons_ext_pb',
1510 'king-addons-fb-sub',
1511 ];
1512
1513 $options = [];
1514 foreach (get_post_types(['public' => true], 'objects') as $slug => $type) {
1515 if (in_array($slug, $skip, true)) {
1516 continue;
1517 }
1518 $options[$slug] = $type->labels->name ?? $slug;
1519 }
1520
1521 return $options;
1522 }
1523
1524 /**
1525 * Public taxonomies plus an empty option.
1526 *
1527 * @return array<string,string>
1528 */
1529 private function get_taxonomy_options(): array
1530 {
1531 $options = ['' => esc_html__('— Select —', 'king-addons')];
1532
1533 foreach (get_taxonomies(['public' => true], 'objects') as $slug => $taxonomy) {
1534 $options[$slug] = $taxonomy->labels->name ?? $slug;
1535 }
1536
1537 return $options;
1538 }
1539
1540 /**
1541 * Comma-separated slugs or IDs.
1542 *
1543 * @param string $value Raw value.
1544 *
1545 * @return array<int,string>
1546 */
1547 private static function split_list(string $value): array
1548 {
1549 $parts = preg_split('/[\s,]+/', $value) ?: [];
1550 $out = [];
1551 foreach ($parts as $part) {
1552 $part = trim((string) $part);
1553 if ('' !== $part) {
1554 $out[] = $part;
1555 }
1556 }
1557
1558 return array_values(array_unique($out));
1559 }
1560
1561 /**
1562 * Comma-separated positive IDs.
1563 *
1564 * @param string $value Raw value.
1565 *
1566 * @return array<int,int>
1567 */
1568 private static function id_list(string $value): array
1569 {
1570 $ids = [];
1571 foreach (self::split_list($value) as $part) {
1572 $id = absint($part);
1573 if ($id > 0) {
1574 $ids[] = $id;
1575 }
1576 }
1577
1578 return array_values(array_unique($ids));
1579 }
1580 }
1581