PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 4.0.0
Tutor LMS – eLearning and online course solution v4.0.0
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 17 hours ago Accordion.php 17 hours ago Alert.php 17 hours ago AttachmentCard.php 17 hours ago Avatar.php 17 hours ago Badge.php 17 hours ago BaseComponent.php 17 hours ago Button.php 17 hours ago ConfirmationModal.php 17 hours ago CourseFilter.php 17 hours ago DateFilter.php 17 hours ago DropdownFilter.php 17 hours ago EmptyState.php 17 hours ago FileUploader.php 17 hours ago InputField.php 17 hours ago Modal.php 17 hours ago Nav.php 17 hours ago Pagination.php 17 hours ago Popover.php 17 hours ago PreviewTrigger.php 17 hours ago Progress.php 17 hours ago SearchFilter.php 17 hours ago Sorting.php 17 hours ago StarRating.php 17 hours ago StarRatingInput.php 17 hours ago StatusSelect.php 17 hours ago SvgIcon.php 17 hours ago Table.php 17 hours ago Tabs.php 17 hours ago Tooltip.php 17 hours ago WPEditor.php 17 hours ago
DropdownFilter.php
579 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 * ```
28 * // Basic dropdown with custom options
29 * DropdownFilter::make()
30 * ->options( $options )
31 * ->query_param( 'status' )
32 * ->variant( Variant::PRIMARY )
33 * ->size( Size::SMALL )
34 * ->search( true )
35 * ->render();
36 *
37 * // Course filter (convenience method)
38 * DropdownFilter::make()
39 * ->options( $courses )
40 * ->count( $total_items )
41 * ->query_param( 'course_id' )
42 * ->variant( Variant::PRIMARY_SOFT )
43 * ->size( Size::X_SMALL )
44 * ->popover_size( Size::MEDIUM )
45 * ->position( Positions::BOTTOM_END )
46 * ->placeholder( __( 'Search Course', 'tutor' ) )
47 * ->render();
48 * ```
49 *
50 * @since 4.0.0
51 */
52 class DropdownFilter extends BaseComponent {
53
54 /**
55 * Options for the dropdown
56 *
57 * @var array
58 */
59 protected $options = array();
60
61 /**
62 * Whether to show search box
63 *
64 * @var bool
65 */
66 protected $show_search = false;
67
68 /**
69 * Trigger button variant
70 *
71 * @var string
72 */
73 protected $variant = Variant::LINK;
74
75 /**
76 * Trigger button size
77 *
78 * @var string
79 */
80 protected $size = Size::MEDIUM;
81
82 /**
83 * Custom button CSS classes
84 *
85 * @var string
86 */
87 protected $button_class = '';
88
89 /**
90 * Search placeholder
91 *
92 * @var string
93 */
94 protected $placeholder = '';
95
96 /**
97 * Query parameter name to manage URL and active state
98 *
99 * @var string
100 */
101 protected $query_param = '';
102
103 /**
104 * Count to display
105 *
106 * @var int|null
107 */
108 protected $count = null;
109
110 /**
111 * Popover size
112 *
113 * @var string
114 */
115 protected $popover_size = Size::SMALL;
116
117 /**
118 * On change callback function name
119 *
120 * @var string
121 */
122 protected $on_change = '';
123
124 /**
125 * Alpine.js variable name for active state
126 *
127 * @var string
128 */
129 protected $active_value = '';
130
131 /**
132 * Base URL for building filter links (preserves other query params when set).
133 *
134 * @var string
135 */
136 protected $base_url = '';
137
138
139 /**
140 * Icon size
141 *
142 * @var int|null
143 */
144 protected $icon_size = null;
145
146 /**
147 * Popover placement position
148 *
149 * @var string
150 */
151 protected $position = Positions::BOTTOM_START;
152
153 /**
154 * Set options list
155 *
156 * @param array $options list of options. Each item: array( 'label' => string, 'url' => string, 'count' => int|null, 'active' => bool, 'value' => mixed ).
157 *
158 * @return self
159 */
160 public function options( array $options ): self {
161 $this->options = $options;
162 return $this;
163 }
164
165 /**
166 * Set query parameter name
167 *
168 * @param string $key query parameter name.
169 *
170 * @return self
171 */
172 public function query_param( string $key ): self {
173 $this->query_param = $key;
174 return $this;
175 }
176
177 /**
178 * Set search visibility
179 *
180 * @param bool $show whether to show search.
181 *
182 * @return self
183 */
184 public function search( bool $show ): self {
185 $this->show_search = $show;
186 return $this;
187 }
188
189 /**
190 * Set button variant
191 *
192 * @param string $variant button variant.
193 *
194 * @return self
195 */
196 public function variant( string $variant ): self {
197 $this->variant = $variant;
198 return $this;
199 }
200
201 /**
202 * Set button size
203 *
204 * @param string $size button size.
205 *
206 * @return self
207 */
208 public function size( string $size ): self {
209 $this->size = $size;
210 return $this;
211 }
212
213 /**
214 * Set button CSS classes
215 *
216 * @param string $classes CSS classes for the button.
217 *
218 * @return self
219 */
220 public function button_class( string $classes ): self {
221 $this->button_class = $classes;
222 return $this;
223 }
224
225 /**
226 * Set search placeholder
227 *
228 * @param string $placeholder search placeholder.
229 *
230 * @return self
231 */
232 public function placeholder( string $placeholder ): self {
233 $this->placeholder = $placeholder;
234 return $this;
235 }
236
237 /**
238 * Set popover width
239 *
240 * @param string $size popover width size (Size::SMALL, Size::MEDIUM, Size::LARGE).
241 *
242 * @return self
243 */
244 public function popover_size( string $size ): self {
245 $this->popover_size = $size;
246 return $this;
247 }
248
249 /**
250 * Set icon size
251 *
252 * @param int $size icon size.
253 *
254 * @return self
255 */
256 public function icon_size( int $size ): self {
257 $this->icon_size = $size;
258 return $this;
259 }
260
261 /**
262 * Set popover placement position.
263 *
264 * @param string $position placement position (Positions::TOP, Positions::BOTTOM, etc).
265 *
266 * @return self
267 */
268 public function position( string $position ): self {
269 $this->position = $position;
270 return $this;
271 }
272
273 /**
274 * Get popover width in pixels based on size constant
275 *
276 * @param string $size Size constant.
277 *
278 * @return string Width in pixels.
279 */
280 protected function get_popover_width( string $size ): string {
281 switch ( $size ) {
282 case Size::MEDIUM:
283 return '275px';
284 case Size::LARGE:
285 return '350px';
286 case Size::SMALL:
287 default:
288 return '172px';
289 }
290 }
291
292 /**
293 * Set count
294 *
295 * @param int $count count to display.
296 *
297 * @return self
298 */
299 public function count( int $count ): self {
300 $this->count = $count;
301 return $this;
302 }
303
304 /**
305 * Set JS callback on change
306 *
307 * @param string $method_name JS method name.
308 *
309 * @return self
310 */
311 public function on_change( string $method_name ): self {
312 $this->on_change = $method_name;
313 return $this;
314 }
315
316 /**
317 * Set Alpine.js active state binding
318 *
319 * @param string $active_value Alpine variable name.
320 *
321 * @return self
322 */
323 public function bind_active_value( string $active_value ): self {
324 $this->active_value = $active_value;
325 return $this;
326 }
327
328 /**
329 * Set base URL for filter links so other query params are preserved (cumulative filtering).
330 *
331 * @param string $url Full current page URL including query string.
332 *
333 * @return self
334 */
335 public function base_url( string $url ): self {
336 $this->base_url = $url;
337 return $this;
338 }
339
340 /**
341 * Get the base URL used for auto-generated filter links.
342 *
343 * Removes transient pagination state so changing filters resets pagination.
344 *
345 * @return string
346 */
347 protected function get_filter_base_url(): string {
348 if ( ! empty( $this->base_url ) ) {
349 return remove_query_arg( 'current_page', $this->base_url );
350 }
351
352 return remove_query_arg( 'current_page' );
353 }
354
355
356 /**
357 * Get component content
358 *
359 * @return string
360 */
361 public function get(): string {
362 $options = $this->options;
363 if ( empty( $options ) ) {
364 return '';
365 }
366
367 // Automatically manage active state and URL if query_param is set.
368 if ( ! empty( $this->query_param ) ) {
369 $current_val = Input::get( $this->query_param, '' );
370 $base = $this->get_filter_base_url();
371 foreach ( $options as &$option ) {
372 $val = isset( $option['value'] ) ? $option['value'] : '';
373 if ( ! isset( $option['active'] ) ) {
374 $option['active'] = (string) $val === (string) $current_val;
375 }
376 if ( ! isset( $option['url'] ) ) {
377 if ( ! empty( $base ) ) {
378 $option['url'] = ! empty( $val ) ? add_query_arg( $this->query_param, $val, $base ) : remove_query_arg( $this->query_param, $base );
379 } else {
380 $option['url'] = ! empty( $val ) ? add_query_arg( $this->query_param, $val ) : remove_query_arg( $this->query_param );
381 }
382 }
383 }
384 unset( $option );
385 }
386
387 $active_option = null;
388
389 foreach ( $options as $option ) {
390 if ( ! empty( $option['active'] ) ) {
391 $active_option = $option;
392 break;
393 }
394 }
395
396 // Fallback to first option if none is active.
397 if ( ! $active_option && ! empty( $options ) ) {
398 $active_option = $options[0];
399 }
400
401 $label = $active_option['label'] ?? '';
402 $count = $active_option['count'] ?? null;
403
404 // If count is set via count() method, use it for the active option.
405 if ( null !== $this->count ) {
406 $count = $this->count;
407 }
408
409 $btn_class = $this->button_class;
410 if ( empty( $btn_class ) ) {
411 $size_class = 'tutor-btn-medium';
412 if ( Size::SMALL === $this->size ) {
413 $size_class = 'tutor-btn-small';
414 } elseif ( Size::X_SMALL === $this->size ) {
415 $size_class = 'tutor-btn-x-small';
416 } elseif ( Size::LARGE === $this->size ) {
417 $size_class = 'tutor-btn-large';
418 }
419
420 if ( Variant::PRIMARY === $this->variant ) {
421 $btn_class = 'tutor-btn tutor-btn-primary ' . $size_class;
422 } elseif ( Variant::PRIMARY_SOFT === $this->variant ) {
423 $btn_class = 'tutor-btn tutor-btn-primary-soft ' . $size_class;
424 } elseif ( Variant::OUTLINE === $this->variant ) {
425 $btn_class = 'tutor-btn tutor-btn-outline ' . $size_class;
426 } else {
427 $btn_class = 'tutor-btn tutor-btn-link';
428 }
429 }
430
431 $origin = Popover::TRANSFORM_ORIGIN_MAP[ $this->position ] ?? 'left.top';
432
433 ob_start();
434 ?>
435 <div
436 class="tutor-dropdown-filter"
437 x-data="{
438 ...tutorPopover({
439 placement: '<?php echo esc_js( $this->position ); ?>',
440 offset: 4,
441 }),
442 <?php if ( $this->active_value ) : ?>
443 labels: <?php echo esc_attr( wp_json_encode( array_column( $options, 'label', 'value' ) ) ); ?>,
444 counts: <?php echo esc_attr( wp_json_encode( array_column( $options, 'count', 'value' ) ) ); ?>
445 <?php endif; ?>
446 }"
447 >
448 <button
449 type="button"
450 x-ref="trigger"
451 @click="toggle()"
452 class="<?php echo esc_attr( $btn_class ); ?>"
453 >
454 <span class="tutor-truncate <?php echo Variant::LINK === $this->variant ? esc_attr( 'tutor-text-secondary' ) : ''; ?>" style="max-width: 150px;"
455 <?php if ( $this->active_value ) : ?>
456 x-text="labels[<?php echo esc_attr( $this->active_value ); ?>] || '<?php echo esc_js( $label ); ?>'"
457 <?php endif; ?>
458 >
459 <?php echo esc_html( $label ); ?>
460 </span>
461 <?php if ( null !== $count || $this->active_value ) : ?>
462 <span class="tutor-font-medium <?php echo Variant::LINK === $this->variant ? esc_attr( 'tutor-text-primary' ) : ''; ?>"
463 <?php if ( $this->active_value ) : ?>
464 x-text="'(' + (counts[<?php echo esc_attr( $this->active_value ); ?>] ?? '0') + ')'"
465 x-show="counts[<?php echo esc_attr( $this->active_value ); ?>] !== null"
466 <?php endif; ?>
467 >
468 <?php echo null !== $count ? '(' . esc_html( $count ) . ')' : ''; ?>
469 </span>
470 <?php endif; ?>
471 <?php
472 $icon_size = $this->icon_size;
473 if ( null === $icon_size ) {
474 $icon_size = in_array( $this->variant, array( Variant::PRIMARY, Variant::PRIMARY_SOFT ), true ) ? 16 : 20;
475 }
476 SvgIcon::make()->name( Icon::CHEVRON_DOWN_2 )->size( $icon_size )->color( Variant::PRIMARY_SOFT === $this->variant ? Color::BRAND : Color::SECONDARY )->render();
477 ?>
478 </button>
479
480 <div
481 x-ref="content"
482 x-show="open"
483 x-cloak
484 x-transition.<?php echo esc_attr( $origin ); ?>
485 @click.outside="handleClickOutside()"
486 class="tutor-popover"
487 style="width: <?php echo esc_attr( $this->get_popover_width( $this->popover_size ) ); ?>; max-width: unset;"
488 >
489 <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')); } }">
490 <?php if ( $this->show_search ) : ?>
491 <div class="tutor-input-field tutor-px-5 tutor-pt-2 tutor-pb-4">
492 <div class="tutor-input-wrapper">
493 <div class="tutor-input-content tutor-input-content-left">
494 <?php SvgIcon::make()->name( Icon::SEARCH_2 )->size( 16 )->color( Color::IDLE )->render(); ?>
495 </div>
496 <input
497 type="text"
498 class="tutor-input tutor-input-sm tutor-input-content-left"
499 placeholder="<?php echo esc_attr( $this->placeholder ? $this->placeholder : __( 'Search...', 'tutor' ) ); ?>"
500 x-model="search"
501 @click.stop
502 >
503 </div>
504 </div>
505 <?php endif; ?>
506
507 <div class="tutor-overflow-y-auto" style="max-height: 200px;">
508 <?php foreach ( $options as $option ) : ?>
509 <?php
510 $opt_label = $option['label'] ?? '';
511 $opt_url = $option['url'] ?? '#';
512 $opt_count = $option['count'] ?? null;
513 $is_active = ! empty( $option['active'] );
514 $val = isset( $option['value'] ) ? $option['value'] : '';
515 ?>
516 <?php if ( $this->on_change ) : ?>
517 <button
518 type="button"
519 @click="<?php echo esc_js( $this->on_change ); ?>('<?php echo esc_attr( $val ); ?>'); hide()"
520 class="tutor-popover-menu-item <?php echo esc_attr( ! $this->active_value && $is_active ? ' tutor-active' : '' ); ?>"
521 <?php if ( $this->active_value ) : ?>
522 :class="{ 'tutor-active': (<?php echo esc_attr( $this->active_value ); ?> || '') === '<?php echo esc_attr( $val ); ?>' }"
523 <?php endif; ?>
524 <?php if ( $this->show_search ) : ?>
525 x-show="search === '' || '<?php echo esc_js( $opt_label ); ?>'.toLowerCase().includes(search.toLowerCase())"
526 <?php endif; ?>
527 >
528 <span class="tutor-flex tutor-gap-2 tutor-items-center tutor-w-full">
529 <span class="tutor-truncate">
530 <?php echo esc_html( $opt_label ); ?>
531 </span>
532 <?php if ( null !== $opt_count ) : ?>
533 <span>
534 (<?php echo esc_html( $opt_count ); ?>)
535 </span>
536 <?php endif; ?>
537 </span>
538 </button>
539 <?php else : ?>
540 <a
541 href="<?php echo esc_url( $opt_url ); ?>"
542 class="tutor-popover-menu-item <?php echo $is_active ? 'tutor-active' : ''; ?>"
543 <?php if ( $this->show_search ) : ?>
544 x-show="search === '' || '<?php echo esc_js( $opt_label ); ?>'.toLowerCase().includes(search.toLowerCase())"
545 <?php endif; ?>
546 >
547 <span class="tutor-flex tutor-gap-2 tutor-items-center tutor-w-full">
548 <span class="tutor-truncate">
549 <?php echo esc_html( $opt_label ); ?>
550 </span>
551 <?php if ( null !== $opt_count ) : ?>
552 <span>
553 (<?php echo esc_html( $opt_count ); ?>)
554 </span>
555 <?php endif; ?>
556 </span>
557 </a>
558 <?php endif; ?>
559 <?php endforeach; ?>
560
561 <?php if ( $this->show_search ) : ?>
562 <div
563 x-show="search !== '' && !hasResults"
564 class="tutor-p-5 tutor-text-center tutor-text-secondary tutor-p2"
565 >
566 <?php esc_html_e( 'No results found', 'tutor' ); ?>
567 </div>
568 <?php endif; ?>
569 </div>
570 </div>
571 </div>
572 </div>
573 <?php
574 $this->component_string = ob_get_clean();
575
576 return $this->component_string;
577 }
578 }
579