PluginProbe ʕ •ᴥ•ʔ
Spider Elements – Premium Elementor Widgets & Addons Library / trunk
Spider Elements – Premium Elementor Widgets & Addons Library vtrunk
trunk 1.0.0 1.1.0 1.5.0 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.7.0 1.8.0 1.9.0
spider-elements / widgets / Dynamic_Faq.php
spider-elements / widgets Last commit date
templates 4 months ago Accordion.php 9 months ago Alerts_Box.php 9 months ago Blog_Grid.php 6 months ago Cheat_Sheet.php 6 months ago Counter.php 6 months ago Dynamic_Faq.php 1 month ago Icon_Box.php 6 months ago Integrations.php 6 months ago List_Item.php 9 months ago Tabs.php 6 months ago Team_Carousel.php 6 months ago Testimonial.php 6 months ago Timeline.php 9 months ago Video_Playlist.php 6 months ago Video_Popup.php 6 months ago
Dynamic_Faq.php
958 lines
1 <?php
2 namespace SPEL\Widgets;
3
4 use Elementor\Widget_Base;
5 use Elementor\Controls_Manager;
6 use Elementor\Icons_Manager;
7 use Elementor\Group_Control_Typography;
8 use Elementor\Group_Control_Box_Shadow;
9 use WP_Query;
10
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 /**
16 * Dynamic FAQ Widget
17 *
18 * Renders posts from any public post type as a collapsible FAQ list.
19 * Supports excerpt/content source, word/char limits, reply counts, and stacked avatars.
20 */
21 class Dynamic_Faq extends Widget_Base {
22
23 public function get_name(): string {
24 return 'spel_dynamic_faq';
25 }
26
27 public function get_title(): string {
28 return esc_html__( 'Dynamic FAQ', 'spider-elements' );
29 }
30
31 public function get_icon(): string {
32 return 'eicon-accordion spel-icon';
33 }
34
35 public function get_keywords(): array {
36 return [ 'spider', 'faq', 'accordion', 'dynamic', 'posts', 'forum', 'docs', 'help', 'questions' ];
37 }
38
39 public function get_categories(): array {
40 return [ 'spider-elements' ];
41 }
42
43 public function get_style_depends(): array {
44 return [ 'spel-main', 'elegant-icon' ];
45 }
46
47 protected function register_controls(): void {
48 $this->elementor_content_control();
49 $this->elementor_style_control();
50 }
51
52 // =========================================================================
53 // CONTENT CONTROLS
54 // =========================================================================
55
56 public function elementor_content_control(): void {
57
58 // =========================================================
59 // 1. QUERY SETTINGS — Data source & retrieval
60 // =========================================================
61 $this->start_controls_section(
62 'query_section',
63 [
64 'label' => esc_html__( 'Query Settings', 'spider-elements' ),
65 'tab' => Controls_Manager::TAB_CONTENT,
66 ]
67 );
68
69 $post_types = get_post_types( [ 'public' => true ], 'objects' );
70 $pt_options = [];
71 foreach ( $post_types as $pt ) {
72 $pt_options[ $pt->name ] = $pt->labels->singular_name;
73 }
74
75 $this->add_control(
76 'post_type',
77 [
78 'label' => esc_html__( 'Source (Post Type)', 'spider-elements' ),
79 'type' => Controls_Manager::SELECT,
80 'default' => 'post',
81 'options' => $pt_options,
82 ]
83 );
84
85 $this->add_control(
86 'posts_per_page',
87 [
88 'label' => esc_html__( 'Number of Items', 'spider-elements' ),
89 'type' => Controls_Manager::NUMBER,
90 'default' => 5,
91 'min' => 1,
92 'max' => 50,
93 ]
94 );
95
96 $this->add_control(
97 'orderby',
98 [
99 'label' => esc_html__( 'Order By', 'spider-elements' ),
100 'type' => Controls_Manager::SELECT,
101 'default' => 'date',
102 'options' => [
103 'date' => esc_html__( 'Date', 'spider-elements' ),
104 'modified' => esc_html__( 'Last Modified', 'spider-elements' ),
105 'title' => esc_html__( 'Title', 'spider-elements' ),
106 'rand' => esc_html__( 'Random', 'spider-elements' ),
107 'comment_count' => esc_html__( 'Comment Count', 'spider-elements' ),
108 ],
109 ]
110 );
111
112 $this->add_control(
113 'order',
114 [
115 'label' => esc_html__( 'Order', 'spider-elements' ),
116 'type' => Controls_Manager::SELECT,
117 'default' => 'DESC',
118 'options' => [
119 'DESC' => esc_html__( 'Descending', 'spider-elements' ),
120 'ASC' => esc_html__( 'Ascending', 'spider-elements' ),
121 ],
122 ]
123 );
124
125 $this->end_controls_section();
126
127 // =========================================================
128 // 2. DISPLAY SETTINGS — Content & visual options
129 // =========================================================
130 $this->start_controls_section(
131 'display_section',
132 [
133 'label' => esc_html__( 'Display Settings', 'spider-elements' ),
134 'tab' => Controls_Manager::TAB_CONTENT,
135 ]
136 );
137
138 $this->add_control(
139 'open_first',
140 [
141 'label' => esc_html__( 'Initially Expanded', 'spider-elements' ),
142 'description' => esc_html__( 'Keep the first FAQ item expanded when the page loads', 'spider-elements' ),
143 'type' => Controls_Manager::SWITCHER,
144 'label_on' => esc_html__( 'Yes', 'spider-elements' ),
145 'label_off' => esc_html__( 'No', 'spider-elements' ),
146 'return_value' => 'yes',
147 'default' => 'yes',
148 ]
149 );
150
151 $this->add_control(
152 'content_source',
153 [
154 'label' => esc_html__( 'Content to Display', 'spider-elements' ),
155 'type' => Controls_Manager::SELECT,
156 'default' => 'content',
157 'options' => [
158 'excerpt' => esc_html__( 'Excerpt', 'spider-elements' ),
159 'content' => esc_html__( 'Full Content', 'spider-elements' ),
160 ],
161 ]
162 );
163
164 $this->add_control(
165 'limit_number',
166 [
167 'label' => esc_html__( 'Word Limit', 'spider-elements' ),
168 'description' => esc_html__( 'Maximum number of words to display. Set to -1 to show all content without truncation.', 'spider-elements' ),
169 'type' => Controls_Manager::NUMBER,
170 'default' => 10,
171 ]
172 );
173
174 $this->add_control(
175 'show_meta',
176 [
177 'label' => esc_html__( 'Show Meta (Count / Date)', 'spider-elements' ),
178 'type' => Controls_Manager::SWITCHER,
179 'label_on' => esc_html__( 'Show', 'spider-elements' ),
180 'label_off' => esc_html__( 'Hide', 'spider-elements' ),
181 'return_value' => 'yes',
182 'default' => 'yes',
183 'separator' => 'before',
184 ]
185 );
186
187 $this->add_control(
188 'show_avatars',
189 [
190 'label' => esc_html__( 'Show Reply Avatars', 'spider-elements' ),
191 'type' => Controls_Manager::SWITCHER,
192 'label_on' => esc_html__( 'Show', 'spider-elements' ),
193 'label_off' => esc_html__( 'Hide', 'spider-elements' ),
194 'return_value' => 'yes',
195 'default' => 'yes',
196 ]
197 );
198
199 $this->add_control(
200 'avatar_count',
201 [
202 'label' => esc_html__( 'Max Avatars', 'spider-elements' ),
203 'type' => Controls_Manager::NUMBER,
204 'default' => 3,
205 'min' => 1,
206 'max' => 10,
207 'condition' => [
208 'show_avatars' => 'yes',
209 ],
210 ]
211 );
212
213 $this->add_control(
214 'show_read_more',
215 [
216 'label' => esc_html__( 'View Details Link', 'spider-elements' ),
217 'type' => Controls_Manager::SWITCHER,
218 'label_on' => esc_html__( 'Show', 'spider-elements' ),
219 'label_off' => esc_html__( 'Hide', 'spider-elements' ),
220 'return_value' => 'yes',
221 'default' => 'yes',
222 'separator' => 'before',
223 ]
224 );
225
226 $this->add_control(
227 'read_more_text',
228 [
229 'label' => esc_html__( 'Link Text', 'spider-elements' ),
230 'type' => Controls_Manager::TEXT,
231 'default' => esc_html__( 'View Details', 'spider-elements' ),
232 'placeholder' => esc_html__( 'View Details', 'spider-elements' ),
233 'condition' => [
234 'show_read_more' => 'yes',
235 ],
236 ]
237 );
238
239 $this->add_control(
240 'read_more_new_tab',
241 [
242 'label' => esc_html__( 'Open in New Tab', 'spider-elements' ),
243 'type' => Controls_Manager::SWITCHER,
244 'return_value' => 'yes',
245 'default' => '',
246 'condition' => [
247 'show_read_more' => 'yes',
248 ],
249 ]
250 );
251
252 $this->end_controls_section();
253 }
254
255 // =========================================================================
256 // STYLE CONTROLS
257 // =========================================================================
258
259 public function elementor_style_control(): void {
260
261 // =========================================================
262 // 1. CARD — item wrapper appearance
263 // =========================================================
264 $this->start_controls_section(
265 'style_item_section',
266 [
267 'label' => esc_html__( 'Card', 'spider-elements' ),
268 'tab' => Controls_Manager::TAB_STYLE,
269 ]
270 );
271
272 $this->add_control(
273 'item_bg_color',
274 [
275 'label' => esc_html__( 'Background', 'spider-elements' ),
276 'type' => Controls_Manager::COLOR,
277 'selectors' => [
278 '{{WRAPPER}} .spel-faq-item' => 'background-color: {{VALUE}};',
279 ],
280 ]
281 );
282
283 $this->add_control(
284 'item_border_color',
285 [
286 'label' => esc_html__( 'Border Color', 'spider-elements' ),
287 'type' => Controls_Manager::COLOR,
288 'selectors' => [
289 '{{WRAPPER}} .spel-faq-item' => 'border-color: {{VALUE}};',
290 ],
291 ]
292 );
293
294 $this->add_responsive_control(
295 'item_border_radius',
296 [
297 'label' => esc_html__( 'Border Radius', 'spider-elements' ),
298 'type' => Controls_Manager::DIMENSIONS,
299 'size_units' => [ 'px', '%', 'em', 'rem' ],
300 'selectors' => [
301 '{{WRAPPER}} .spel-faq-item' => 'border-radius: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
302 ],
303 ]
304 );
305
306 $this->add_group_control(
307 Group_Control_Box_Shadow::get_type(),
308 [
309 'name' => 'item_box_shadow',
310 'selector' => '{{WRAPPER}} .spel-faq-item',
311 ]
312 );
313
314 $this->add_responsive_control(
315 'item_gap',
316 [
317 'label' => esc_html__( 'Spacing Between Cards', 'spider-elements' ),
318 'type' => Controls_Manager::SLIDER,
319 'size_units' => [ 'px', 'em', 'rem' ],
320 'range' => [
321 'px' => [ 'min' => 0, 'max' => 60 ],
322 ],
323 'selectors' => [
324 '{{WRAPPER}} .spel-faq-item' => 'margin-bottom: {{SIZE}}{{UNIT}};',
325 ],
326 ]
327 );
328
329 $this->end_controls_section();
330
331 // =========================================================
332 // 2. THUMBNAIL — featured image icon box
333 // =========================================================
334 $this->start_controls_section(
335 'style_icon_section',
336 [
337 'label' => esc_html__( 'Thumbnail', 'spider-elements' ),
338 'tab' => Controls_Manager::TAB_STYLE,
339 ]
340 );
341
342 $this->add_control(
343 'icon_color',
344 [
345 'label' => esc_html__( 'Icon Color', 'spider-elements' ),
346 'type' => Controls_Manager::COLOR,
347 'selectors' => [
348 '{{WRAPPER}} .spel-faq-icon' => 'color: {{VALUE}};',
349 '{{WRAPPER}} .spel-faq-icon svg' => 'stroke: {{VALUE}}; fill: {{VALUE}};',
350 ],
351 ]
352 );
353
354 $this->add_control(
355 'icon_bg_color',
356 [
357 'label' => esc_html__( 'Background', 'spider-elements' ),
358 'type' => Controls_Manager::COLOR,
359 'selectors' => [
360 '{{WRAPPER}} .spel-faq-icon' => 'background-color: {{VALUE}};',
361 ],
362 ]
363 );
364
365 $this->add_responsive_control(
366 'icon_box_size',
367 [
368 'label' => esc_html__( 'Box Size', 'spider-elements' ),
369 'type' => Controls_Manager::SLIDER,
370 'size_units' => [ 'px', 'em', 'rem' ],
371 'range' => [
372 'px' => [ 'min' => 24, 'max' => 80 ],
373 ],
374 'selectors' => [
375 '{{WRAPPER}} .spel-faq-icon' => 'width: {{SIZE}}{{UNIT}}; height: {{SIZE}}{{UNIT}};',
376 ],
377 ]
378 );
379
380 $this->add_responsive_control(
381 'icon_font_size',
382 [
383 'label' => esc_html__( 'Thumbnail Size', 'spider-elements' ),
384 'type' => Controls_Manager::SLIDER,
385 'size_units' => [ 'px', 'em', 'rem' ],
386 'range' => [
387 'px' => [ 'min' => 10, 'max' => 48 ],
388 ],
389 'selectors' => [
390 '{{WRAPPER}} .spel-faq-icon i' => 'font-size: {{SIZE}}{{UNIT}};',
391 '{{WRAPPER}} .spel-faq-icon svg' => 'width: {{SIZE}}{{UNIT}}; height: {{SIZE}}{{UNIT}};',
392 ],
393 ]
394 );
395
396 $this->add_responsive_control(
397 'icon_border_radius',
398 [
399 'label' => esc_html__( 'Border Radius', 'spider-elements' ),
400 'type' => Controls_Manager::DIMENSIONS,
401 'size_units' => [ 'px', '%', 'em', 'rem' ],
402 'selectors' => [
403 '{{WRAPPER}} .spel-faq-icon' => 'border-radius: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
404 ],
405 ]
406 );
407
408 $this->end_controls_section();
409
410 // =========================================================
411 // 3. TYPOGRAPHY — all text groups in one place
412 // =========================================================
413 $this->start_controls_section(
414 'style_content_section',
415 [
416 'label' => esc_html__( 'Content', 'spider-elements' ),
417 'tab' => Controls_Manager::TAB_STYLE,
418 ]
419 );
420
421 $this->add_control(
422 'title_heading',
423 [
424 'label' => esc_html__( 'Question Title', 'spider-elements' ),
425 'type' => Controls_Manager::HEADING,
426 ]
427 );
428
429 $this->add_control(
430 'title_color',
431 [
432 'label' => esc_html__( 'Color', 'spider-elements' ),
433 'type' => Controls_Manager::COLOR,
434 'selectors' => [
435 '{{WRAPPER}} .spel-faq-summary-text h3' => 'color: {{VALUE}};',
436 ],
437 ]
438 );
439
440 $this->add_group_control(
441 Group_Control_Typography::get_type(),
442 [
443 'name' => 'title_typography',
444 'selector' => '{{WRAPPER}} .spel-faq-summary-text h3',
445 ]
446 );
447
448 $this->add_control(
449 'meta_heading',
450 [
451 'label' => esc_html__( 'Meta Info', 'spider-elements' ),
452 'type' => Controls_Manager::HEADING,
453 'separator' => 'before',
454 ]
455 );
456
457 $this->add_control(
458 'meta_color',
459 [
460 'label' => esc_html__( 'Color', 'spider-elements' ),
461 'type' => Controls_Manager::COLOR,
462 'selectors' => [
463 '{{WRAPPER}} .spel-faq-summary-meta' => 'color: {{VALUE}};',
464 ],
465 ]
466 );
467
468 $this->add_group_control(
469 Group_Control_Typography::get_type(),
470 [
471 'name' => 'meta_typography',
472 'selector' => '{{WRAPPER}} .spel-faq-summary-meta',
473 ]
474 );
475
476 $this->add_control(
477 'excerpt_heading',
478 [
479 'label' => esc_html__( 'Answer Text', 'spider-elements' ),
480 'type' => Controls_Manager::HEADING,
481 'separator' => 'before',
482 ]
483 );
484
485 $this->add_control(
486 'excerpt_color',
487 [
488 'label' => esc_html__( 'Color', 'spider-elements' ),
489 'type' => Controls_Manager::COLOR,
490 'selectors' => [
491 '{{WRAPPER}} .spel-faq-body p' => 'color: {{VALUE}};',
492 ],
493 ]
494 );
495
496 $this->add_group_control(
497 Group_Control_Typography::get_type(),
498 [
499 'name' => 'excerpt_typography',
500 'selector' => '{{WRAPPER}} .spel-faq-body p',
501 ]
502 );
503
504 // — Reply footer text ("Mira, Jules and 45 others…") —
505 $this->add_control(
506 'reply_footer_heading',
507 [
508 'label' => esc_html__( 'Reply Footer', 'spider-elements' ),
509 'type' => Controls_Manager::HEADING,
510 'separator' => 'before',
511 ]
512 );
513
514 $this->add_control(
515 'helpful_text_color',
516 [
517 'label' => esc_html__( 'Color', 'spider-elements' ),
518 'type' => Controls_Manager::COLOR,
519 'selectors' => [
520 '{{WRAPPER}} .spel-faq-reply' => 'color: {{VALUE}};',
521 ],
522 ]
523 );
524
525 $this->add_group_control(
526 Group_Control_Typography::get_type(),
527 [
528 'name' => 'helpful_typography',
529 'selector' => '{{WRAPPER}} .spel-faq-reply',
530 ]
531 );
532
533 $this->end_controls_section();
534
535 // =========================================================
536 // 4. AVATARS — stacked commenter / author images
537 // =========================================================
538 $this->start_controls_section(
539 'style_replies_section',
540 [
541 'label' => esc_html__( 'Avatars', 'spider-elements' ),
542 'tab' => Controls_Manager::TAB_STYLE,
543 ]
544 );
545
546 $this->add_responsive_control(
547 'avatar_size',
548 [
549 'label' => esc_html__( 'Size', 'spider-elements' ),
550 'type' => Controls_Manager::SLIDER,
551 'size_units' => [ 'px', 'em', 'rem' ],
552 'range' => [
553 'px' => [ 'min' => 16, 'max' => 60 ],
554 ],
555 'selectors' => [
556 '{{WRAPPER}} .spel-avatar-stack span' => 'width: {{SIZE}}{{UNIT}}; height: {{SIZE}}{{UNIT}};',
557 ],
558 ]
559 );
560
561 $this->add_control(
562 'avatar_border_color',
563 [
564 'label' => esc_html__( 'Border Color', 'spider-elements' ),
565 'type' => Controls_Manager::COLOR,
566 'selectors' => [
567 '{{WRAPPER}} .spel-avatar-stack span' => 'border-color: {{VALUE}};',
568 ],
569 ]
570 );
571
572 $this->end_controls_section();
573
574 // =========================================================
575 // 5. TOGGLE ICON — the expand / collapse indicator
576 // =========================================================
577 $this->start_controls_section(
578 'style_chevron_section',
579 [
580 'label' => esc_html__( 'Toggle Icon', 'spider-elements' ),
581 'tab' => Controls_Manager::TAB_STYLE,
582 ]
583 );
584
585 $this->add_control(
586 'chev_icon',
587 [
588 'label' => esc_html__( 'Collapsed Icon', 'spider-elements' ),
589 'type' => Controls_Manager::ICONS,
590 'default' => [
591 'value' => 'fas fa-chevron-down',
592 'library' => 'fa-solid',
593 ],
594 ]
595 );
596
597 $this->add_control(
598 'chev_icon_active',
599 [
600 'label' => esc_html__( 'Expanded Icon', 'spider-elements' ),
601 'type' => Controls_Manager::ICONS,
602 'default' => [
603 'value' => 'fas fa-chevron-up',
604 'library' => 'fa-solid',
605 ],
606 ]
607 );
608
609 $this->add_responsive_control(
610 'chev_size',
611 [
612 'label' => esc_html__( 'Size', 'spider-elements' ),
613 'type' => Controls_Manager::SLIDER,
614 'size_units' => [ 'px', 'em', 'rem' ],
615 'range' => [
616 'px' => [ 'min' => 10, 'max' => 36 ],
617 ],
618 'default' => [
619 'size' => 14,
620 'unit' => 'px',
621 ],
622 'selectors' => [
623 '{{WRAPPER}} .spel-faq-chev i' => 'font-size: {{SIZE}}{{UNIT}};',
624 '{{WRAPPER}} .spel-faq-chev svg' => 'width: {{SIZE}}{{UNIT}}; height: {{SIZE}}{{UNIT}};',
625 ],
626 ]
627 );
628
629 $this->add_control(
630 'chev_color',
631 [
632 'label' => esc_html__( 'Color', 'spider-elements' ),
633 'type' => Controls_Manager::COLOR,
634 'selectors' => [
635 '{{WRAPPER}} .spel-faq-chev' => 'color: {{VALUE}};',
636 '{{WRAPPER}} .spel-faq-chev i' => 'color: {{VALUE}};',
637 ],
638 ]
639 );
640
641 $this->add_control(
642 'chev_open_color',
643 [
644 'label' => esc_html__( 'Active Color', 'spider-elements' ),
645 'type' => Controls_Manager::COLOR,
646 'selectors' => [
647 '{{WRAPPER}} .spel-faq-item[open] .spel-faq-chev' => 'color: {{VALUE}};',
648 '{{WRAPPER}} .spel-faq-item[open] .spel-faq-chev i' => 'color: {{VALUE}};',
649 ],
650 ]
651 );
652
653 $this->end_controls_section();
654
655 // =========================================================
656 // 6. VIEW DETAILS — the "read more" link
657 // =========================================================
658 $this->start_controls_section(
659 'style_read_more_section',
660 [
661 'label' => esc_html__( 'View Details', 'spider-elements' ),
662 'tab' => Controls_Manager::TAB_STYLE,
663 'condition' => [
664 'show_read_more' => 'yes',
665 ],
666 ]
667 );
668
669 $this->add_control(
670 'read_more_icon',
671 [
672 'label' => esc_html__( 'Icon', 'spider-elements' ),
673 'type' => Controls_Manager::ICONS,
674 'default' => [
675 'value' => 'fas fa-arrow-right',
676 'library' => 'fa-solid',
677 ],
678 ]
679 );
680
681 $this->add_control(
682 'read_more_color',
683 [
684 'label' => esc_html__( 'Color', 'spider-elements' ),
685 'type' => Controls_Manager::COLOR,
686 'separator' => 'before',
687 'selectors' => [
688 '{{WRAPPER}} .spel-faq-read-more' => 'color: {{VALUE}};',
689 ],
690 ]
691 );
692
693 $this->add_control(
694 'read_more_hover_color',
695 [
696 'label' => esc_html__( 'Hover Color', 'spider-elements' ),
697 'type' => Controls_Manager::COLOR,
698 'selectors' => [
699 '{{WRAPPER}} .spel-faq-read-more:hover' => 'color: {{VALUE}};',
700 ],
701 ]
702 );
703
704 $this->add_group_control(
705 Group_Control_Typography::get_type(),
706 [
707 'name' => 'read_more_typography',
708 'selector' => '{{WRAPPER}} .spel-faq-read-more',
709 ]
710 );
711
712 $this->end_controls_section();
713 }
714
715 // =========================================================================
716 // RENDER
717 // =========================================================================
718
719 protected function render(): void {
720 $settings = $this->get_settings_for_display();
721 $post_type = sanitize_key( $settings['post_type'] ?? 'post' );
722
723 $allowed_orderby = [ 'date', 'modified', 'title', 'rand', 'comment_count' ];
724 $orderby = in_array( $settings['orderby'], $allowed_orderby, true ) ? $settings['orderby'] : 'date';
725 $order = 'ASC' === $settings['order'] ? 'ASC' : 'DESC';
726
727 $query = new WP_Query( [
728 'post_type' => $post_type,
729 'posts_per_page' => absint( $settings['posts_per_page'] ),
730 'post_status' => 'publish',
731 'orderby' => $orderby,
732 'order' => $order,
733 'no_found_rows' => true,
734 'ignore_sticky_posts' => true,
735 ] );
736
737 if ( ! $query->have_posts() ) {
738 echo '<p>' . esc_html__( 'No posts found.', 'spider-elements' ) . '</p>';
739 return;
740 }
741
742 // Post types that use "replies" rather than "comments".
743 $forum_types = [ 'forum', 'docs', 'topic', 'reply', 'doc', 'bbp_topic', 'bbp_reply' ];
744 $is_forum = in_array( $post_type, $forum_types, true );
745 $open_first = 'yes' === ( $settings['open_first'] ?? 'yes' );
746 $show_meta = 'yes' === ( $settings['show_meta'] ?? 'yes' );
747 $show_avatars = 'yes' === ( $settings['show_avatars'] ?? 'yes' );
748 $avatar_limit = absint( $settings['avatar_count'] ?? 3 );
749 $show_read_more = 'yes' === ( $settings['show_read_more'] ?? 'yes' );
750 $read_more_text = ! empty( $settings['read_more_text'] )
751 ? $settings['read_more_text']
752 : esc_html__( 'View Details', 'spider-elements' );
753 $read_more_new_tab = 'yes' === ( $settings['read_more_new_tab'] ?? '' );
754 $index = 0;
755
756 echo '<div class="spel-faq-wrap">';
757
758 while ( $query->have_posts() ) {
759 $query->the_post();
760
761 $content = $this->get_limited_content( $settings );
762 $has_thumb = has_post_thumbnail();
763 $item_classes = 'spel-faq-item' . ( ! $has_thumb ? ' spel-faq-item--no-icon' : '' );
764
765 ?>
766 <details class="<?php echo esc_attr( $item_classes ); ?>"<?php echo ( $open_first && 0 === $index ) ? ' open' : ''; ?>>
767 <summary class="spel-faq-summary">
768 <?php if ( $has_thumb ) : ?>
769 <span class="spel-faq-icon">
770 <?php the_post_thumbnail( [ 38, 38 ] ); ?>
771 </span>
772 <?php endif; ?>
773 <div class="spel-faq-summary-text">
774 <h3><?php the_title(); ?></h3>
775 <?php if ( $show_meta ) : ?>
776 <div class="spel-faq-summary-meta">
777 <?php echo wp_kses_post( $this->get_meta_text( $is_forum ) ); ?>
778 </div>
779 <?php endif; ?>
780 </div>
781 <span class="spel-faq-chev" aria-hidden="true">
782 <?php if ( ! empty( $settings['chev_icon']['value'] ) ) : ?>
783 <span class="spel-faq-chev-icon spel-faq-chev-collapsed">
784 <?php Icons_Manager::render_icon( $settings['chev_icon'], [ 'aria-hidden' => 'true' ] ); ?>
785 </span>
786 <?php else : ?>
787 <svg class="spel-faq-chev-icon spel-faq-chev-collapsed" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 9l6 6 6-6"/></svg>
788 <?php endif; ?>
789 <?php if ( ! empty( $settings['chev_icon_active']['value'] ) ) : ?>
790 <span class="spel-faq-chev-icon spel-faq-chev-active">
791 <?php Icons_Manager::render_icon( $settings['chev_icon_active'], [ 'aria-hidden' => 'true' ] ); ?>
792 </span>
793 <?php else : ?>
794 <svg class="spel-faq-chev-icon spel-faq-chev-active" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 9l6-6 6 6"/></svg>
795 <?php endif; ?>
796 </span>
797 </summary>
798 <div class="spel-faq-body">
799 <?php if ( $content ) : ?>
800 <p><?php echo wp_kses_post( $content ); ?></p>
801 <?php endif; ?>
802 <?php if ( $show_avatars ) : ?>
803 <?php $this->render_reply_row( get_the_ID(), $avatar_limit ); ?>
804 <?php endif; ?>
805 <?php if ( $show_read_more ) : ?>
806 <a href="<?php the_permalink(); ?>" class="spel-faq-read-more"<?php if ( $read_more_new_tab ) : ?> target="_blank" rel="noopener noreferrer"<?php endif; ?>>
807 <?php echo esc_html( $read_more_text ); ?>
808 <?php if ( ! empty( $settings['read_more_icon']['value'] ) ) : ?>
809 <?php Icons_Manager::render_icon( $settings['read_more_icon'], [ 'aria-hidden' => 'true' ] ); ?>
810 <?php else : ?>
811 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
812 <?php endif; ?>
813 </a>
814 <?php endif; ?>
815 </div>
816 </details>
817 <?php
818
819 $index++;
820 }
821
822 echo '</div>';
823 wp_reset_postdata();
824 }
825
826 /**
827 * Returns the content string, trimmed to the configured word limit.
828 * A limit of -1 (or any value < 1) means show all content.
829 */
830 private function get_limited_content( array $settings ): string {
831 if ( 'content' === $settings['content_source'] ) {
832 $content = get_the_content();
833 } else {
834 $content = get_the_excerpt();
835 // Fall back to full content when no excerpt is set.
836 if ( '' === trim( $content ) ) {
837 $content = get_the_content();
838 }
839 }
840
841 $content = wp_strip_all_tags( $content );
842 $limit = (int) ( $settings['limit_number'] ?? -1 );
843
844 if ( $limit > 0 ) {
845 $content = wp_trim_words( $content, $limit, '&hellip;' );
846 }
847
848 return $content;
849 }
850
851 /**
852 * Builds the meta line: "Answered · N replies · updated X ago".
853 * Handles singular/plural and zero-count grammar correctly.
854 */
855 private function get_meta_text( bool $is_forum ): string {
856 $count = (int) get_comments_number();
857 $time_diff = human_time_diff( (int) get_the_modified_time( 'U' ), time() );
858
859 if ( $is_forum ) {
860 if ( 0 === $count ) {
861 $count_text = esc_html__( 'No reply', 'spider-elements' );
862 } elseif ( 1 === $count ) {
863 $count_text = esc_html__( '1 reply', 'spider-elements' );
864 } else {
865 /* translators: %s: number of replies */
866 $count_text = sprintf( esc_html__( '%s replies', 'spider-elements' ), number_format_i18n( $count ) );
867 }
868 } else {
869 if ( 0 === $count ) {
870 $count_text = esc_html__( 'No comment', 'spider-elements' );
871 } elseif ( 1 === $count ) {
872 $count_text = esc_html__( '1 comment', 'spider-elements' );
873 } else {
874 /* translators: %s: number of comments */
875 $count_text = sprintf( esc_html__( '%s comments', 'spider-elements' ), number_format_i18n( $count ) );
876 }
877 }
878
879 return sprintf(
880 /* translators: 1: count phrase (e.g. "3 replies"), 2: human time diff */
881 __( 'Answered &middot; %1$s &middot; updated %2$s ago', 'spider-elements' ),
882 $count_text,
883 $time_diff
884 );
885 }
886
887 /**
888 * Renders the stacked-avatar row with names and helpful count.
889 */
890 private function render_reply_row( int $post_id, int $limit ): void {
891 // Fetch without type filter so forum replies, topic replies, etc. are included.
892 $comments = get_comments( [
893 'post_id' => $post_id,
894 'number' => $limit,
895 'status' => 'approve',
896 ] );
897
898 // Build avatar stack: post author first, then commenters.
899 $avatars = [];
900
901 $author_id = (int) get_the_author_meta( 'ID' );
902 $author_url = get_avatar_url( $author_id, [ 'size' => 44 ] );
903 if ( $author_url ) {
904 $avatars[] = [ 'url' => $author_url, 'name' => get_the_author() ];
905 }
906
907 foreach ( $comments as $comment ) {
908 if ( count( $avatars ) >= $limit ) {
909 break;
910 }
911 $url = get_avatar_url( $comment, [ 'size' => 44 ] );
912 if ( $url && $url !== $author_url ) {
913 $avatars[] = [ 'url' => $url, 'name' => $comment->comment_author ];
914 }
915 }
916
917 echo '<div class="spel-faq-reply">';
918 echo '<div class="spel-avatar-stack">';
919
920 foreach ( $avatars as $avatar ) {
921 printf(
922 '<span style="background-image:url(%s);" title="%s"></span>',
923 esc_url( $avatar['url'] ),
924 esc_attr( $avatar['name'] )
925 );
926 }
927
928 echo '</div>'; // .spel-avatar-stack
929
930 echo '<span>';
931
932 if ( ! empty( $comments ) ) {
933 $names = array_map( static fn( $c ) => $c->comment_author, array_slice( $comments, 0, 2 ) );
934 $others = max( 0, get_comments_number( $post_id ) - 2 );
935
936 if ( $others > 0 ) {
937 printf(
938 /* translators: 1: Comma-separated names, 2: extra count */
939 esc_html__( '%1$s and %2$s others found this helpful', 'spider-elements' ),
940 esc_html( implode( ', ', $names ) ),
941 absint( $others )
942 );
943 } else {
944 printf(
945 /* translators: 1: Name or names */
946 esc_html__( '%s found this helpful', 'spider-elements' ),
947 esc_html( implode( ' &amp; ', $names ) )
948 );
949 }
950 } else {
951 echo esc_html__( 'Be the first to reply', 'spider-elements' );
952 }
953
954 echo '</span>';
955 echo '</div>'; // .spel-faq-reply
956 }
957 }
958