PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 4.0.3
Tutor LMS – eLearning and online course solution v4.0.3
4.0.3 4.0.2 4.0.1 4.0.0 3.9.15 3.9.14 3.9.13 3.9.12 3.9.11 trunk 1.0.0 1.0.0-alpha 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.2.0 1.2.1 1.2.11 1.2.12 1.2.13 1.2.20 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 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.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 1.7.6 1.7.7 1.7.8 1.7.9 1.8.0 1.8.1 1.8.10 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7 1.8.8 1.8.9 1.9.0 1.9.1 1.9.10 1.9.11 1.9.12 1.9.13 1.9.14 1.9.15 1.9.16 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.1 2.0.10 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1.0 2.1.1 2.1.10 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.3.0 2.4.0 2.5.0 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 3.0.0 3.0.1 3.0.2 3.1.0 3.2.0 3.2.1 3.2.2 3.2.3 3.3.0 3.3.1 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.7.0 3.7.1 3.7.2 3.7.3 3.7.4 3.8.0 3.8.1 3.8.2 3.8.3 3.9.0 3.9.1 3.9.10 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9
tutor / components / DropdownFilter.php
tutor / components Last commit date
Constants 3 weeks ago Accordion.php 6 days ago Alert.php 6 days ago AttachmentCard.php 6 days ago Avatar.php 6 days ago Badge.php 6 days ago BaseComponent.php 3 weeks ago Button.php 6 days ago ConfirmationModal.php 6 days ago CourseFilter.php 6 days ago DateFilter.php 6 days ago DropdownFilter.php 6 days ago EmptyState.php 6 days ago FileUploader.php 6 days ago InputField.php 6 days ago Modal.php 6 days ago Nav.php 6 days ago Pagination.php 6 days ago Popover.php 6 days ago PreviewTrigger.php 6 days ago Progress.php 6 days ago SearchFilter.php 6 days ago Sorting.php 6 days ago StarRating.php 6 days ago StarRatingInput.php 6 days ago StatusSelect.php 6 days ago SvgIcon.php 6 days ago Table.php 6 days ago Tabs.php 6 days ago Tooltip.php 6 days ago WPEditor.php 6 days ago
DropdownFilter.php
623 lines
1 <?php
2 /**
3 * DropdownFilter Component Class.
4 *
5 * @package Tutor\Components
6 * @author Themeum
7 * @link https://themeum.com
8 * @since 4.0.0
9 */
10
11 namespace Tutor\Components;
12
13 use Tutor\Components\Constants\Size;
14 use Tutor\Components\Constants\Variant;
15 use Tutor\Components\Constants\Positions;
16 use Tutor\Components\Constants\Color;
17 use Tutor\Components\Popover;
18 use TUTOR\Icon;
19 use TUTOR\Input;
20
21 defined( 'ABSPATH' ) || exit;
22
23 /**
24 * Class DropdownFilter
25 *
26 * Example Usage:
27 * ```php
28 * // Basic dropdown bound to URL query param (link-based navigation)
29 * DropdownFilter::make()
30 * ->options( $options )
31 * ->query_param( 'status' )
32 * ->render();
33 *
34 * // With custom variant, size and search box
35 * DropdownFilter::make()
36 * ->options( $options )
37 * ->query_param( 'status' )
38 * ->variant( Variant::PRIMARY )
39 * ->size( Size::SMALL )
40 * ->search( true )
41 * ->placeholder( __( 'Search status...', 'tutor' ) )
42 * ->render();
43 *
44 * // JS callback on change (Alpine.js method name) instead of link navigation
45 * DropdownFilter::make()
46 * ->options( $options )
47 * ->on_change( 'changeStatus' )
48 * ->bind_active_value( 'currentStatus' )
49 * ->render();
50 *
51 * // Course filter convenience use-case
52 * DropdownFilter::make()
53 * ->options( CourseFilter::get_course_filter_options() )
54 * ->count( $total_items )
55 * ->query_param( 'course-id' )
56 * ->variant( Variant::PRIMARY_SOFT )
57 * ->size( Size::X_SMALL )
58 * ->popover_size( Size::MEDIUM )
59 * ->position( Positions::BOTTOM_END )
60 * ->placeholder( __( 'Search Course', 'tutor' ) )
61 * ->search( true )
62 * ->render();
63 *
64 * // Cumulative filtering — preserve existing query params via base_url
65 * DropdownFilter::make()
66 * ->options( $options )
67 * ->query_param( 'category' )
68 * ->base_url( remove_query_arg( 'current_page' ) )
69 * ->render();
70 *
71 * // Outline variant with custom button CSS
72 * DropdownFilter::make()
73 * ->options( $options )
74 * ->variant( Variant::OUTLINE )
75 * ->button_class( 'tutor-btn tutor-btn-outline tutor-btn-small my-custom-btn' )
76 * ->render();
77 *
78 * // Popover positioned bottom-end with large width
79 * DropdownFilter::make()
80 * ->options( $options )
81 * ->query_param( 'type' )
82 * ->position( Positions::BOTTOM_END )
83 * ->popover_size( Size::LARGE )
84 * ->render();
85 *
86 * // Custom icon size for the chevron
87 * DropdownFilter::make()
88 * ->options( $options )
89 * ->variant( Variant::PRIMARY )
90 * ->icon_size( 14 )
91 * ->render();
92 * ```
93 *
94 * @since 4.0.0
95 */
96 class DropdownFilter extends BaseComponent {
97
98 /**
99 * Options for the dropdown
100 *
101 * @var array
102 */
103 protected $options = array();
104
105 /**
106 * Whether to show search box
107 *
108 * @var bool
109 */
110 protected $show_search = false;
111
112 /**
113 * Trigger button variant
114 *
115 * @var string
116 */
117 protected $variant = Variant::LINK;
118
119 /**
120 * Trigger button size
121 *
122 * @var string
123 */
124 protected $size = Size::MEDIUM;
125
126 /**
127 * Custom button CSS classes
128 *
129 * @var string
130 */
131 protected $button_class = '';
132
133 /**
134 * Search placeholder
135 *
136 * @var string
137 */
138 protected $placeholder = '';
139
140 /**
141 * Query parameter name to manage URL and active state
142 *
143 * @var string
144 */
145 protected $query_param = '';
146
147 /**
148 * Count to display
149 *
150 * @var int|null
151 */
152 protected $count = null;
153
154 /**
155 * Popover size
156 *
157 * @var string
158 */
159 protected $popover_size = Size::SMALL;
160
161 /**
162 * On change callback function name
163 *
164 * @var string
165 */
166 protected $on_change = '';
167
168 /**
169 * Alpine.js variable name for active state
170 *
171 * @var string
172 */
173 protected $active_value = '';
174
175 /**
176 * Base URL for building filter links (preserves other query params when set).
177 *
178 * @var string
179 */
180 protected $base_url = '';
181
182
183 /**
184 * Icon size
185 *
186 * @var int|null
187 */
188 protected $icon_size = null;
189
190 /**
191 * Popover placement position
192 *
193 * @var string
194 */
195 protected $position = Positions::BOTTOM_START;
196
197 /**
198 * Set options list
199 *
200 * @param array $options list of options. Each item: array( 'label' => string, 'url' => string, 'count' => int|null, 'active' => bool, 'value' => mixed ).
201 *
202 * @return self
203 */
204 public function options( array $options ): self {
205 $this->options = $options;
206 return $this;
207 }
208
209 /**
210 * Set query parameter name
211 *
212 * @param string $key query parameter name.
213 *
214 * @return self
215 */
216 public function query_param( string $key ): self {
217 $this->query_param = $key;
218 return $this;
219 }
220
221 /**
222 * Set search visibility
223 *
224 * @param bool $show whether to show search.
225 *
226 * @return self
227 */
228 public function search( bool $show ): self {
229 $this->show_search = $show;
230 return $this;
231 }
232
233 /**
234 * Set button variant
235 *
236 * @param string $variant button variant.
237 *
238 * @return self
239 */
240 public function variant( string $variant ): self {
241 $this->variant = $variant;
242 return $this;
243 }
244
245 /**
246 * Set button size
247 *
248 * @param string $size button size.
249 *
250 * @return self
251 */
252 public function size( string $size ): self {
253 $this->size = $size;
254 return $this;
255 }
256
257 /**
258 * Set button CSS classes
259 *
260 * @param string $classes CSS classes for the button.
261 *
262 * @return self
263 */
264 public function button_class( string $classes ): self {
265 $this->button_class = $classes;
266 return $this;
267 }
268
269 /**
270 * Set search placeholder
271 *
272 * @param string $placeholder search placeholder.
273 *
274 * @return self
275 */
276 public function placeholder( string $placeholder ): self {
277 $this->placeholder = $placeholder;
278 return $this;
279 }
280
281 /**
282 * Set popover width
283 *
284 * @param string $size popover width size (Size::SMALL, Size::MEDIUM, Size::LARGE).
285 *
286 * @return self
287 */
288 public function popover_size( string $size ): self {
289 $this->popover_size = $size;
290 return $this;
291 }
292
293 /**
294 * Set icon size
295 *
296 * @param int $size icon size.
297 *
298 * @return self
299 */
300 public function icon_size( int $size ): self {
301 $this->icon_size = $size;
302 return $this;
303 }
304
305 /**
306 * Set popover placement position.
307 *
308 * @param string $position placement position (Positions::TOP, Positions::BOTTOM, etc).
309 *
310 * @return self
311 */
312 public function position( string $position ): self {
313 $this->position = $position;
314 return $this;
315 }
316
317 /**
318 * Get popover width in pixels based on size constant
319 *
320 * @param string $size Size constant.
321 *
322 * @return string Width in pixels.
323 */
324 protected function get_popover_width( string $size ): string {
325 switch ( $size ) {
326 case Size::MEDIUM:
327 return '275px';
328 case Size::LARGE:
329 return '350px';
330 case Size::SMALL:
331 default:
332 return '172px';
333 }
334 }
335
336 /**
337 * Set count
338 *
339 * @param int $count count to display.
340 *
341 * @return self
342 */
343 public function count( int $count ): self {
344 $this->count = $count;
345 return $this;
346 }
347
348 /**
349 * Set JS callback on change
350 *
351 * @param string $method_name JS method name.
352 *
353 * @return self
354 */
355 public function on_change( string $method_name ): self {
356 $this->on_change = $method_name;
357 return $this;
358 }
359
360 /**
361 * Set Alpine.js active state binding
362 *
363 * @param string $active_value Alpine variable name.
364 *
365 * @return self
366 */
367 public function bind_active_value( string $active_value ): self {
368 $this->active_value = $active_value;
369 return $this;
370 }
371
372 /**
373 * Set base URL for filter links so other query params are preserved (cumulative filtering).
374 *
375 * @param string $url Full current page URL including query string.
376 *
377 * @return self
378 */
379 public function base_url( string $url ): self {
380 $this->base_url = $url;
381 return $this;
382 }
383
384 /**
385 * Get the base URL used for auto-generated filter links.
386 *
387 * Removes transient pagination state so changing filters resets pagination.
388 *
389 * @return string
390 */
391 protected function get_filter_base_url(): string {
392 if ( ! empty( $this->base_url ) ) {
393 return remove_query_arg( 'current_page', $this->base_url );
394 }
395
396 return remove_query_arg( 'current_page' );
397 }
398
399
400 /**
401 * Get component content
402 *
403 * @return string
404 */
405 public function get(): string {
406 $options = $this->options;
407 if ( empty( $options ) ) {
408 return '';
409 }
410
411 // Automatically manage active state and URL if query_param is set.
412 if ( ! empty( $this->query_param ) ) {
413 $current_val = Input::get( $this->query_param, '' );
414 $base = $this->get_filter_base_url();
415 foreach ( $options as &$option ) {
416 $val = isset( $option['value'] ) ? $option['value'] : '';
417 if ( ! isset( $option['active'] ) ) {
418 $option['active'] = (string) $val === (string) $current_val;
419 }
420 if ( ! isset( $option['url'] ) ) {
421 if ( ! empty( $base ) ) {
422 $option['url'] = ! empty( $val ) ? add_query_arg( $this->query_param, $val, $base ) : remove_query_arg( $this->query_param, $base );
423 } else {
424 $option['url'] = ! empty( $val ) ? add_query_arg( $this->query_param, $val ) : remove_query_arg( $this->query_param );
425 }
426 }
427 }
428 unset( $option );
429 }
430
431 $active_option = null;
432
433 foreach ( $options as $option ) {
434 if ( ! empty( $option['active'] ) ) {
435 $active_option = $option;
436 break;
437 }
438 }
439
440 // Fallback to first option if none is active.
441 if ( ! $active_option && ! empty( $options ) ) {
442 $active_option = $options[0];
443 }
444
445 $label = $active_option['label'] ?? '';
446 $count = $active_option['count'] ?? null;
447
448 // If count is set via count() method, use it for the active option.
449 if ( null !== $this->count ) {
450 $count = $this->count;
451 }
452
453 $btn_class = $this->button_class;
454 if ( empty( $btn_class ) ) {
455 $size_class = 'tutor-btn-medium';
456 if ( Size::SMALL === $this->size ) {
457 $size_class = 'tutor-btn-small';
458 } elseif ( Size::X_SMALL === $this->size ) {
459 $size_class = 'tutor-btn-x-small';
460 } elseif ( Size::LARGE === $this->size ) {
461 $size_class = 'tutor-btn-large';
462 }
463
464 if ( Variant::PRIMARY === $this->variant ) {
465 $btn_class = 'tutor-btn tutor-btn-primary ' . $size_class;
466 } elseif ( Variant::PRIMARY_SOFT === $this->variant ) {
467 $btn_class = 'tutor-btn tutor-btn-primary-soft ' . $size_class;
468 } elseif ( Variant::OUTLINE === $this->variant ) {
469 $btn_class = 'tutor-btn tutor-btn-outline ' . $size_class;
470 } else {
471 $btn_class = 'tutor-btn tutor-btn-link';
472 }
473 }
474
475 $origin = Popover::TRANSFORM_ORIGIN_MAP[ $this->position ] ?? 'left.top';
476
477 ob_start();
478 ?>
479 <div
480 class="tutor-dropdown-filter"
481 x-data="{
482 ...tutorPopover({
483 placement: '<?php echo esc_js( $this->position ); ?>',
484 offset: 4,
485 }),
486 <?php if ( $this->active_value ) : ?>
487 labels: <?php echo esc_attr( wp_json_encode( array_column( $options, 'label', 'value' ) ) ); ?>,
488 counts: <?php echo esc_attr( wp_json_encode( array_column( $options, 'count', 'value' ) ) ); ?>
489 <?php endif; ?>
490 }"
491 >
492 <button
493 type="button"
494 x-ref="trigger"
495 @click="toggle()"
496 class="<?php echo esc_attr( $btn_class ); ?>"
497 >
498 <span class="tutor-truncate <?php echo Variant::LINK === $this->variant ? esc_attr( 'tutor-text-secondary' ) : ''; ?>" style="max-width: 150px;"
499 <?php if ( $this->active_value ) : ?>
500 x-text="labels[<?php echo esc_attr( $this->active_value ); ?>] || '<?php echo esc_js( $label ); ?>'"
501 <?php endif; ?>
502 >
503 <?php echo esc_html( $label ); ?>
504 </span>
505 <?php if ( null !== $count || $this->active_value ) : ?>
506 <span class="tutor-font-medium <?php echo Variant::LINK === $this->variant ? esc_attr( 'tutor-text-primary' ) : ''; ?>"
507 <?php if ( $this->active_value ) : ?>
508 x-text="'(' + (counts[<?php echo esc_attr( $this->active_value ); ?>] ?? '0') + ')'"
509 x-show="counts[<?php echo esc_attr( $this->active_value ); ?>] !== null"
510 <?php endif; ?>
511 >
512 <?php echo null !== $count ? '(' . esc_html( $count ) . ')' : ''; ?>
513 </span>
514 <?php endif; ?>
515 <?php
516 $icon_size = $this->icon_size;
517 if ( null === $icon_size ) {
518 $icon_size = in_array( $this->variant, array( Variant::PRIMARY, Variant::PRIMARY_SOFT ), true ) ? 16 : 20;
519 }
520 SvgIcon::make()->name( Icon::CHEVRON_DOWN_2 )->size( $icon_size )->color( Variant::PRIMARY_SOFT === $this->variant ? Color::BRAND : Color::SECONDARY )->render();
521 ?>
522 </button>
523
524 <div
525 x-ref="content"
526 x-show="open"
527 x-cloak
528 x-transition.<?php echo esc_attr( $origin ); ?>
529 @click.outside="handleClickOutside()"
530 class="tutor-popover"
531 style="width: <?php echo esc_attr( $this->get_popover_width( $this->popover_size ) ); ?>; max-width: unset;"
532 >
533 <div class="tutor-popover-menu" x-data="{ search: '', get hasResults() { return this.search === '' || [...$el.querySelectorAll('.tutor-popover-menu-item')].some(el => el.style.display !== 'none' && !el.hasAttribute('hidden')); } }">
534 <?php if ( $this->show_search ) : ?>
535 <div class="tutor-input-field tutor-px-5 tutor-pt-2 tutor-pb-4">
536 <div class="tutor-input-wrapper">
537 <div class="tutor-input-content tutor-input-content-left">
538 <?php SvgIcon::make()->name( Icon::SEARCH_2 )->size( 16 )->color( Color::IDLE )->render(); ?>
539 </div>
540 <input
541 type="text"
542 class="tutor-input tutor-input-sm tutor-input-content-left"
543 placeholder="<?php echo esc_attr( $this->placeholder ? $this->placeholder : __( 'Search...', 'tutor' ) ); ?>"
544 x-model="search"
545 @click.stop
546 >
547 </div>
548 </div>
549 <?php endif; ?>
550
551 <div class="tutor-overflow-y-auto" style="max-height: 200px;">
552 <?php foreach ( $options as $option ) : ?>
553 <?php
554 $opt_label = $option['label'] ?? '';
555 $opt_url = $option['url'] ?? '#';
556 $opt_count = $option['count'] ?? null;
557 $is_active = ! empty( $option['active'] );
558 $val = isset( $option['value'] ) ? $option['value'] : '';
559 ?>
560 <?php if ( $this->on_change ) : ?>
561 <button
562 type="button"
563 @click="<?php echo esc_js( $this->on_change ); ?>('<?php echo esc_attr( $val ); ?>'); hide()"
564 class="tutor-popover-menu-item <?php echo esc_attr( ! $this->active_value && $is_active ? ' tutor-active' : '' ); ?>"
565 <?php if ( $this->active_value ) : ?>
566 :class="{ 'tutor-active': (<?php echo esc_attr( $this->active_value ); ?> || '') === '<?php echo esc_attr( $val ); ?>' }"
567 <?php endif; ?>
568 <?php if ( $this->show_search ) : ?>
569 x-show="search === '' || '<?php echo esc_js( $opt_label ); ?>'.toLowerCase().includes(search.toLowerCase())"
570 <?php endif; ?>
571 >
572 <span class="tutor-flex tutor-gap-2 tutor-items-center tutor-w-full">
573 <span class="tutor-truncate">
574 <?php echo esc_html( $opt_label ); ?>
575 </span>
576 <?php if ( null !== $opt_count ) : ?>
577 <span>
578 (<?php echo esc_html( $opt_count ); ?>)
579 </span>
580 <?php endif; ?>
581 </span>
582 </button>
583 <?php else : ?>
584 <a
585 href="<?php echo esc_url( $opt_url ); ?>"
586 class="tutor-popover-menu-item <?php echo $is_active ? 'tutor-active' : ''; ?>"
587 <?php if ( $this->show_search ) : ?>
588 x-show="search === '' || '<?php echo esc_js( $opt_label ); ?>'.toLowerCase().includes(search.toLowerCase())"
589 <?php endif; ?>
590 >
591 <span class="tutor-flex tutor-gap-2 tutor-items-center tutor-w-full">
592 <span class="tutor-truncate">
593 <?php echo esc_html( $opt_label ); ?>
594 </span>
595 <?php if ( null !== $opt_count ) : ?>
596 <span>
597 (<?php echo esc_html( $opt_count ); ?>)
598 </span>
599 <?php endif; ?>
600 </span>
601 </a>
602 <?php endif; ?>
603 <?php endforeach; ?>
604
605 <?php if ( $this->show_search ) : ?>
606 <div
607 x-show="search !== '' && !hasResults"
608 class="tutor-p-5 tutor-text-center tutor-text-secondary tutor-p2"
609 >
610 <?php esc_html_e( 'No results found', 'tutor' ); ?>
611 </div>
612 <?php endif; ?>
613 </div>
614 </div>
615 </div>
616 </div>
617 <?php
618 $this->component_string = ob_get_clean();
619
620 return $this->component_string;
621 }
622 }
623