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 / InputField.php
tutor / components Last commit date
Constants 3 weeks ago Accordion.php 1 week ago Alert.php 1 week ago AttachmentCard.php 1 week ago Avatar.php 1 week ago Badge.php 1 week ago BaseComponent.php 3 weeks ago Button.php 1 week ago ConfirmationModal.php 1 week ago CourseFilter.php 1 week ago DateFilter.php 1 week ago DropdownFilter.php 1 week ago EmptyState.php 1 week ago FileUploader.php 1 week ago InputField.php 1 week ago Modal.php 1 week ago Nav.php 1 week ago Pagination.php 1 week ago Popover.php 1 week ago PreviewTrigger.php 1 week ago Progress.php 1 week ago SearchFilter.php 1 week ago Sorting.php 1 week ago StarRating.php 1 week ago StarRatingInput.php 1 week ago StatusSelect.php 1 week ago SvgIcon.php 1 week ago Table.php 1 week ago Tabs.php 1 week ago Tooltip.php 1 week ago WPEditor.php 1 week ago
InputField.php
2234 lines
1 <?php
2 /**
3 * Tutor Component: InputField
4 *
5 * Provides a fluent builder for rendering various input field types with
6 * labels, validation states, and helper text.
7 *
8 * @package Tutor\Components
9 * @author Themeum
10 * @link https://themeum.com
11 * @since 4.0.0
12 */
13
14 namespace Tutor\Components;
15
16 defined( 'ABSPATH' ) || exit;
17
18 use TUTOR\Icon;
19 use ReflectionClass;
20 use Tutor\Components\Constants\Size;
21 use Tutor\Components\Constants\InputType;
22
23 /**
24 * InputField Component Class.
25 *
26 * Example usage:
27 * ```php
28 * // Plain text input with label
29 * InputField::make()
30 * ->type( 'text' )
31 * ->name( 'full_name' )
32 * ->label( 'Full Name' )
33 * ->placeholder( 'Enter your full name' )
34 * ->render();
35 *
36 * // Required text input with clear button and help text
37 * InputField::make()
38 * ->type( 'text' )
39 * ->name( 'full_name' )
40 * ->label( 'Full Name' )
41 * ->placeholder( 'Enter your full name' )
42 * ->required()
43 * ->clearable()
44 * ->help_text( 'This name will appear on your certificate.' )
45 * ->render();
46 *
47 * // Text input with Alpine.js form validation binding
48 * InputField::make()
49 * ->type( 'text' )
50 * ->name( 'username' )
51 * ->label( 'Username' )
52 * ->required()
53 * ->attr( 'x-bind', "register('username', { required: 'Username is required', minLength: { value: 3, message: 'Min 3 characters' } })" )
54 * ->render();
55 *
56 * // Text input with left icon
57 * InputField::make()
58 * ->type( 'text' )
59 * ->name( 'email' )
60 * ->label( 'Email' )
61 * ->left_icon( SvgIcon::make()->name( Icon::MAIL )->size( 18 )->get() )
62 * ->render();
63 *
64 * // Text input with right icon
65 * InputField::make()
66 * ->type( 'text' )
67 * ->name( 'website' )
68 * ->label( 'Website' )
69 * ->right_icon( SvgIcon::make()->name( Icon::LINK )->size( 18 )->get() )
70 * ->render();
71 *
72 * // Email input
73 * InputField::make()
74 * ->type( 'email' )
75 * ->name( 'email_address' )
76 * ->label( 'Email Address' )
77 * ->placeholder( 'you@example.com' )
78 * ->render();
79 *
80 * // Number input
81 * InputField::make()
82 * ->type( 'number' )
83 * ->name( 'seats' )
84 * ->label( 'Seats Available' )
85 * ->value( '30' )
86 * ->render();
87 *
88 * // Password input with strength meter
89 * InputField::make()
90 * ->type( 'password' )
91 * ->name( 'new_password' )
92 * ->label( 'New Password' )
93 * ->show_strength( true )
94 * ->min_strength( 3 )
95 * ->render();
96 *
97 * // Textarea
98 * InputField::make()
99 * ->type( 'textarea' )
100 * ->name( 'bio' )
101 * ->label( 'Bio' )
102 * ->placeholder( 'Tell us about yourself...' )
103 * ->render();
104 *
105 * // Checkbox
106 * InputField::make()
107 * ->type( 'checkbox' )
108 * ->name( 'agree' )
109 * ->label( 'I agree to the terms and conditions' )
110 * ->size( Size::MD )
111 * ->render();
112 *
113 * // Checkbox with HTML label (e.g., link inside label)
114 * InputField::make()
115 * ->type( 'checkbox' )
116 * ->name( 'agree' )
117 * ->label_html( 'I agree to the <a href="/terms">Terms</a>' )
118 * ->render();
119 *
120 * // Intermediate (indeterminate) checkbox
121 * InputField::make()
122 * ->type( 'checkbox' )
123 * ->name( 'select_all' )
124 * ->label( 'Select All' )
125 * ->intermediate( true )
126 * ->render();
127 *
128 * // Pre-checked checkbox
129 * InputField::make()
130 * ->type( 'checkbox' )
131 * ->name( 'subscribe' )
132 * ->label( 'Subscribe to newsletter' )
133 * ->checked( true )
134 * ->render();
135 *
136 * // Radio button
137 * InputField::make()
138 * ->type( 'radio' )
139 * ->name( 'gender' )
140 * ->label( 'Male' )
141 * ->value( 'male' )
142 * ->render();
143 *
144 * // Toggle switch
145 * InputField::make()
146 * ->type( 'switch' )
147 * ->name( 'notifications' )
148 * ->label( 'Enable email notifications' )
149 * ->size( Size::MD )
150 * ->render();
151 *
152 * // Input with error state
153 * InputField::make()
154 * ->type( 'text' )
155 * ->name( 'username' )
156 * ->label( 'Username' )
157 * ->error( 'This field is required.' )
158 * ->render();
159 *
160 * // Disabled input
161 * InputField::make()
162 * ->type( 'text' )
163 * ->name( 'readonly_field' )
164 * ->label( 'Read Only' )
165 * ->value( 'Cannot change me' )
166 * ->disabled()
167 * ->render();
168 *
169 * // Loading state (e.g., while async options load)
170 * InputField::make()
171 * ->type( 'select' )
172 * ->name( 'category' )
173 * ->label( 'Category' )
174 * ->loading( true )
175 * ->loading_message( __( 'Loading categories...', 'tutor' ) )
176 * ->render();
177 *
178 * // Single select with flat options list
179 * InputField::make()
180 * ->type( 'select' )
181 * ->name( 'country' )
182 * ->label( 'Country' )
183 * ->placeholder( 'Select a country' )
184 * ->options( array(
185 * array( 'label' => 'United States', 'value' => 'us' ),
186 * array( 'label' => 'United Kingdom', 'value' => 'uk' ),
187 * array( 'label' => 'Canada', 'value' => 'ca' ),
188 * ) )
189 * ->render();
190 *
191 * // Select with icons and descriptions per option
192 * InputField::make()
193 * ->type( 'select' )
194 * ->name( 'level' )
195 * ->label( 'Course Level' )
196 * ->options( array(
197 * array( 'label' => 'Beginner', 'value' => 'beginner', 'icon' => Icon::PLAY_LINE, 'description' => 'No prior knowledge needed' ),
198 * array( 'label' => 'Intermediate', 'value' => 'intermediate', 'icon' => Icon::BOOK, 'description' => 'Some experience required' ),
199 * array( 'label' => 'Advanced', 'value' => 'advanced', 'icon' => Icon::GRADUATION, 'description' => 'For experienced learners' ),
200 * ) )
201 * ->render();
202 *
203 * // Multi-select with search, clearable and max selections
204 * InputField::make()
205 * ->type( 'select' )
206 * ->name( 'interests' )
207 * ->label( 'Interests' )
208 * ->placeholder( 'Select your interests' )
209 * ->required( 'Please select an interest' )
210 * ->clearable()
211 * ->options( $interests )
212 * ->multiple()
213 * ->searchable()
214 * ->size( Size::MD )
215 * ->max_selections( 2 )
216 * ->help_text( 'You can pick up to 2 interests.' )
217 * ->render();
218 *
219 * // Grouped select options
220 * InputField::make()
221 * ->type( 'select' )
222 * ->name( 'topic' )
223 * ->label( 'Topic' )
224 * ->groups( array(
225 * array(
226 * 'label' => 'Development',
227 * 'options' => array(
228 * array( 'label' => 'PHP', 'value' => 'php' ),
229 * array( 'label' => 'JavaScript', 'value' => 'js' ),
230 * ),
231 * ),
232 * array(
233 * 'label' => 'Design',
234 * 'options' => array(
235 * array( 'label' => 'Figma', 'value' => 'figma' ),
236 * array( 'label' => 'Illustrator','value' => 'ai' ),
237 * ),
238 * ),
239 * ) )
240 * ->render();
241 *
242 * // Select with onChange callback and collapsable on selection
243 * InputField::make()
244 * ->type( 'select' )
245 * ->name( 'sort_by' )
246 * ->label( 'Sort By' )
247 * ->options( $sort_options )
248 * ->on_change( 'function(value){ handleSortChange(value); }' )
249 * ->collapsable( true )
250 * ->render();
251 *
252 * // Time picker (12-hour, 15-minute interval)
253 * InputField::make()
254 * ->type( 'time' )
255 * ->name( 'start_time' )
256 * ->label( 'Start Time' )
257 * ->selection_time_mode( 12 )
258 * ->interval( 15 )
259 * ->render();
260 *
261 * // Small-size input
262 * InputField::make()
263 * ->type( 'text' )
264 * ->name( 'search_term' )
265 * ->placeholder( 'Search...' )
266 * ->size( Size::SM )
267 * ->render();
268 * ```
269 *
270 * @since 4.0.0
271 */
272 class InputField extends BaseComponent {
273
274 /**
275 * InputField type (text|email|password|number|textarea|checkbox|radio|switch|time).
276 *
277 * @since 4.0.0
278 *
279 * @var string
280 */
281 protected $type = InputType::TEXT;
282
283 /**
284 * InputField name attribute.
285 *
286 * @since 4.0.0
287 *
288 * @var string
289 */
290 protected $name = '';
291
292 /**
293 * InputField ID attribute.
294 *
295 * @since 4.0.0
296 *
297 * @var string
298 */
299 protected $id = '';
300
301 /**
302 * InputField value.
303 *
304 * @since 4.0.0
305 *
306 * @var string
307 */
308 protected $value = '';
309
310 /**
311 * InputField label text.
312 *
313 * @since 4.0.0
314 *
315 * @var string
316 */
317 protected $label = '';
318
319 /**
320 * InputField label HTML (for checkbox/radio labels that contain HTML markup).
321 * When set, this takes precedence over $label for checkbox/radio/switch types.
322 *
323 * @since 4.0.0
324 *
325 * @var string
326 */
327 protected $label_html = '';
328
329 /**
330 * InputField placeholder text.
331 *
332 * @since 4.0.0
333 *
334 * @var string
335 */
336 protected $placeholder = '';
337
338 /**
339 * Search Input placeholder text.
340 *
341 * @since 4.0.0
342 *
343 * @var string
344 */
345 protected $search_placeholder = '';
346
347 /**
348 * Help text below input.
349 *
350 * @since 4.0.0
351 *
352 * @var string
353 */
354 protected $help_text = '';
355
356 /**
357 * Error message text.
358 *
359 * @since 4.0.0
360 *
361 * @var string
362 */
363 protected $error = '';
364
365 /**
366 * Whether input is required.
367 *
368 * @since 4.0.0
369 *
370 * @var bool
371 */
372 protected $required = false;
373
374 /**
375 * Input field attr like alpine attribute for validation
376 *
377 * @since 4.0.0
378 *
379 * @var string
380 */
381 protected $attr = '';
382
383 /**
384 * Whether input is disabled.
385 *
386 * @since 4.0.0
387 *
388 * @var bool
389 */
390 protected $disabled = false;
391
392 /**
393 * Whether input is loading
394 *
395 * @since 4.0.0
396 *
397 * @var boolean
398 */
399 protected $loading = false;
400
401 /**
402 * Whether input is checked (checkbox/radio/switch).
403 *
404 * @since 4.0.0
405 *
406 * @var bool
407 */
408 protected $checked = false;
409
410 /**
411 * Whether checkbox is intermediate state.
412 *
413 * @since 4.0.0
414 *
415 * @var bool
416 */
417 protected $intermediate = false;
418
419 /**
420 * Whether to use WordPress media library instead of native file input.
421 *
422 * @since 4.0.0
423 *
424 * @var bool
425 */
426 protected $use_wp_media = false;
427
428 /**
429 * WordPress media modal title.
430 *
431 * @since 4.0.0
432 *
433 * @var string
434 */
435 protected $wp_media_title = '';
436
437 /**
438 * WordPress media modal button text.
439 *
440 * @since 4.0.0
441 *
442 * @var string
443 */
444 protected $wp_media_button_text = '';
445
446 /**
447 * WordPress media library type filter.
448 *
449 * @since 4.0.0
450 *
451 * @var string
452 */
453 protected $wp_media_library_type = '';
454
455 /**
456 * Component variant.
457 *
458 * @since 4.0.0
459 *
460 * @var string
461 */
462 protected $variant = '';
463
464 /**
465 * Whether input has clear button.
466 *
467 * @since 4.0.0
468 *
469 * @var bool
470 */
471 protected $clearable = false;
472
473 /**
474 * Whether input is searchable.
475 *
476 * @since 4.0.0
477 *
478 * @var boolean
479 */
480 protected $searchable = false;
481
482 /**
483 * Whether multiple option can be selected in input.
484 *
485 * @since 4.0.0
486 *
487 * @var boolean
488 */
489 protected $multiple = false;
490
491 /**
492 * Left icon SVG markup.
493 *
494 * @since 4.0.0
495 *
496 * @var string
497 */
498 protected $left_icon = '';
499
500 /**
501 * Right icon SVG markup.
502 *
503 * @since 4.0.0
504 *
505 * @var string
506 */
507 protected $right_icon = '';
508
509 /**
510 * InputField size (sm|md).
511 *
512 * @since 4.0.0
513 *
514 * @var string
515 */
516 protected $size = Size::MD;
517
518 /**
519 * Options for select input field.
520 *
521 * @since 4.0.0
522 *
523 * @var array
524 */
525 protected $options = array();
526
527
528
529 /**
530 * Grouped option for input field.
531 *
532 * @since 4.0.0
533 *
534 * @var array
535 */
536 protected $groups = array();
537
538 /**
539 * Max number of input option to be selected.
540 *
541 * @since 4.0.0
542 *
543 * @var integer
544 */
545 protected $max_selections = 0;
546
547 /**
548 * Close select input on selecting option.
549 *
550 * @since 4.0.0
551 *
552 * @var boolean
553 */
554 protected $close_on_select = null;
555
556 /**
557 * Raw JS onChange callback for select input.
558 *
559 * @since 4.0.0
560 *
561 * @var string|null
562 */
563 protected $on_change = null;
564
565 /**
566 * Max Height for select input.
567 *
568 * @since 4.0.0
569 *
570 * @var integer
571 */
572 protected $max_height = 280;
573
574 /**
575 * Empty state message for input.
576 *
577 * @since 4.0.0
578 *
579 * @var string
580 */
581 protected $empty_message = '';
582
583 /**
584 * Loading state message for input.
585 *
586 * @since 4.0.0
587 *
588 * @var string
589 */
590 protected $loading_message = '';
591
592 /**
593 * Selection time mode (12|24).
594 *
595 * @since 4.0.0
596 *
597 * @var int|null
598 */
599 protected $selection_time_mode = null;
600
601 /**
602 * Time input option interval in minutes.
603 *
604 * @since 4.0.0
605 *
606 * @var int
607 */
608 protected $interval = 30;
609
610 /**
611 * Whether to show password strength meter.
612 *
613 * @since 4.0.0
614 *
615 * @var bool
616 */
617 protected $show_strength = false;
618
619 /**
620 * Minimum password strength required.
621 *
622 * @since 4.0.0
623 *
624 * @var int
625 */
626 protected $min_strength = 3;
627
628 /**
629 * Set input type.
630 *
631 * @since 4.0.0
632 *
633 * @param string $type InputField type.
634 *
635 * @return $this
636 */
637 public function type( $type ) {
638 $allowed = $this->get_allowed_types();
639 if ( in_array( $type, $allowed, true ) ) {
640 $this->type = $type;
641 }
642 return $this;
643 }
644
645 /**
646 * Get allowed input types
647 *
648 * @since 4.0.0
649 *
650 * @return array
651 */
652 public function get_allowed_types() {
653 $class = new ReflectionClass( InputType::class );
654 $constants = $class->getConstants();
655
656 return array_values( $constants );
657 }
658
659 /**
660 * Set input name.
661 *
662 * @since 4.0.0
663 *
664 * @param string $name InputField name.
665 *
666 * @return $this
667 */
668 public function name( $name ) {
669 $this->name = sanitize_text_field( $name );
670 return $this;
671 }
672
673 /**
674 * Set input ID.
675 *
676 * @since 4.0.0
677 *
678 * @param string $id InputField ID.
679 *
680 * @return $this
681 */
682 public function id( $id ) {
683 $this->id = sanitize_text_field( $id );
684 return $this;
685 }
686
687 /**
688 * Set input value.
689 *
690 * @since 4.0.0
691 *
692 * @param string $value InputField value.
693 *
694 * @return $this
695 */
696 public function value( $value ) {
697 $this->value = $value;
698 return $this;
699 }
700
701 /**
702 * Set input label.
703 *
704 * @since 4.0.0
705 *
706 * @param string $label Label text.
707 *
708 * @return $this
709 */
710 public function label( $label ) {
711 $this->label = $label;
712 return $this;
713 }
714
715 /**
716 * Set input label as HTML markup (for checkbox/radio/switch labels containing links or other HTML).
717 * Content is sanitized via wp_kses with a safe set of allowed tags.
718 *
719 * @since 4.0.0
720 *
721 * @param string $html Raw HTML label content.
722 * @param array $allowed_tags Optional. Allowed HTML tags. Defaults to a, span, strong, em.
723 *
724 * @return $this
725 */
726 public function label_html( $html, $allowed_tags = array() ) {
727 if ( empty( $allowed_tags ) ) {
728 $allowed_tags = array(
729 'a' => array(
730 'href' => true,
731 'target' => true,
732 'class' => true,
733 'rel' => true,
734 ),
735 'span' => array( 'class' => true ),
736 'strong' => array(),
737 'em' => array(),
738 );
739 }
740 $this->label_html = wp_kses( $html, $allowed_tags );
741 return $this;
742 }
743
744 /**
745 * Set input placeholder.
746 *
747 * @since 4.0.0
748 *
749 * @param string $placeholder Placeholder text.
750 * @param bool $search whether placeholder is for search input.
751 *
752 * @return $this
753 */
754 public function placeholder( $placeholder, $search = false ) {
755 if ( $search ) {
756 $this->search_placeholder = $placeholder;
757 } else {
758 $this->placeholder = $placeholder;
759 }
760 return $this;
761 }
762
763 /**
764 * Set the help text with HTML sanitization.
765 *
766 * @since 4.0.0
767 *
768 * @param string $text Help text.
769 * @param array $allowed_tags Optional. Allowed HTML tags and attributes for wp_kses().
770 *
771 * @return $this
772 */
773 public function help_text( $text, $allowed_tags = array() ) {
774
775 if ( ! empty( $allowed_tags ) && is_array( $allowed_tags ) ) {
776 $this->help_text = wp_kses( $text, $allowed_tags );
777 return $this;
778 }
779
780 $this->help_text = esc_html( $text );
781 return $this;
782 }
783
784 /**
785 * Set the max height for select input
786 *
787 * @since 4.0.0
788 *
789 * @param integer $max_height the max height.
790 *
791 * @return self
792 */
793 public function max_height( $max_height = 280 ): self {
794 $this->max_height = $max_height;
795 return $this;
796 }
797
798 /**
799 * Collapse select input on selecting option.
800 *
801 * @since 4.0.0
802 *
803 * @param boolean $close_on_select close on selecting option.
804 *
805 * @return self
806 */
807 public function collapsable( $close_on_select = true ): self {
808 $this->close_on_select = $close_on_select;
809 return $this;
810 }
811
812 /**
813 * Set raw JS onChange callback for select input.
814 *
815 * Example: ->on_change( 'function(value){ console.log(value); }' )
816 *
817 * @since 4.0.0
818 *
819 * @param string $on_change Raw JS function string.
820 *
821 * @return self
822 */
823 public function on_change( $on_change ): self {
824 $this->on_change = $on_change;
825 return $this;
826 }
827
828 /**
829 * Set error message.
830 *
831 * @since 4.0.0
832 *
833 * @param string $error Error message.
834 *
835 * @return $this
836 */
837 public function error( $error ) {
838 $this->error = $error;
839 return $this;
840 }
841
842 /**
843 * Set required state.
844 *
845 * @since 4.0.0
846 *
847 * @param bool|string $required Whether input is required.
848 *
849 * @return $this
850 */
851 public function required( $required = true ) {
852 $this->required = $required;
853 return $this;
854 }
855
856 /**
857 * Set disabled state.
858 *
859 * @since 4.0.0
860 *
861 * @param bool $disabled Whether input is disabled.
862 *
863 * @return $this
864 */
865 public function disabled( $disabled = true ) {
866 $this->disabled = (bool) $disabled;
867 return $this;
868 }
869
870 /**
871 * Set loading state.
872 *
873 * @since 4.0.0
874 *
875 * @param bool $loading Whether input is loading.
876 *
877 * @return $this
878 */
879 public function loading( $loading = true ) {
880 $this->loading = (bool) $loading;
881 return $this;
882 }
883
884 /**
885 * Set checked state.
886 *
887 * @since 4.0.0
888 *
889 * @param bool $checked Whether input is checked.
890 *
891 * @return $this
892 */
893 public function checked( $checked = true ) {
894 $this->checked = (bool) $checked;
895 return $this;
896 }
897
898 /**
899 * Set intermediate state for checkbox.
900 *
901 * @since 4.0.0
902 *
903 * @param bool $intermediate Whether checkbox is intermediate.
904 *
905 * @return $this
906 */
907 public function intermediate( $intermediate = true ) {
908 $this->intermediate = $intermediate;
909
910 return $this;
911 }
912
913 /**
914 * Enable WordPress media library instead of native file input.
915 *
916 * @since 4.0.0
917 *
918 * @param bool $use_wp_media Whether to use WordPress media library.
919 *
920 * @return $this
921 */
922 public function use_wp_media( $use_wp_media = true ) {
923 $this->use_wp_media = (bool) $use_wp_media;
924
925 return $this;
926 }
927
928 /**
929 * Set WordPress media modal title.
930 *
931 * @since 4.0.0
932 *
933 * @param string $title Modal title.
934 *
935 * @return $this
936 */
937 public function wp_media_title( $title ) {
938 $this->wp_media_title = $title;
939
940 return $this;
941 }
942
943 /**
944 * Set WordPress media modal button text.
945 *
946 * @since 4.0.0
947 *
948 * @param string $text Button text.
949 *
950 * @return $this
951 */
952 public function wp_media_button_text( $text ) {
953 $this->wp_media_button_text = $text;
954
955 return $this;
956 }
957
958 /**
959 * Set WordPress media library type filter.
960 *
961 * @since 4.0.0
962 *
963 * @param string $type Library type (image, video, audio, application).
964 *
965 * @return $this
966 */
967 public function wp_media_library_type( $type ) {
968 $this->wp_media_library_type = $type;
969
970 return $this;
971 }
972
973 /**
974 * Set component variant.
975 *
976 * @since 4.0.0
977 *
978 * @param string $variant Component variant.
979 *
980 * @return $this
981 */
982 public function variant( $variant ) {
983 $this->variant = $variant;
984
985 return $this;
986 }
987
988 /**
989 * Enable clear button.
990 *
991 * @since 4.0.0
992 *
993 * @param bool $clearable Whether to show clear button.
994 *
995 * @return $this
996 */
997 public function clearable( $clearable = true ) {
998 $this->clearable = (bool) $clearable;
999 return $this;
1000 }
1001
1002 /**
1003 * Set left icon.
1004 *
1005 * @since 4.0.0
1006 *
1007 * @param string $icon SVG icon markup.
1008 *
1009 * @return $this
1010 */
1011 public function left_icon( $icon ) {
1012 $this->left_icon = $icon;
1013 return $this;
1014 }
1015
1016 /**
1017 * Set right icon.
1018 *
1019 * @since 4.0.0
1020 *
1021 * @param string $icon SVG icon markup.
1022 *
1023 * @return $this
1024 */
1025 public function right_icon( $icon ) {
1026 $this->right_icon = $icon;
1027 return $this;
1028 }
1029
1030 /**
1031 * Set input size.
1032 *
1033 * @since 4.0.0
1034 *
1035 * @param string $size Size (sm|md).
1036 *
1037 * @return $this
1038 */
1039 public function size( $size ) {
1040 $allowed = array( 'sm', 'md', 'lg' );
1041 if ( in_array( $size, $allowed, true ) ) {
1042 $this->size = $size;
1043 }
1044 return $this;
1045 }
1046
1047 /**
1048 * Set whether multiple select enabled or not.
1049 *
1050 * @since 4.0.0
1051 *
1052 * @param boolean $multiple whether multiple select is enable.
1053 *
1054 * @return self
1055 */
1056 public function multiple( $multiple = true ): self {
1057 $this->multiple = $multiple;
1058 return $this;
1059 }
1060
1061 /**
1062 * Set if input field is searchable.
1063 *
1064 * @since 4.0.0
1065 *
1066 * @param boolean $searchable whether input is searchable.
1067 *
1068 * @return self
1069 */
1070 public function searchable( $searchable = true ): self {
1071 $this->searchable = $searchable;
1072 return $this;
1073 }
1074
1075 /**
1076 * Set empty message for input.
1077 *
1078 * @since 4.0.0
1079 *
1080 * @param string $empty_message the empty message.
1081 *
1082 * @return self
1083 */
1084 public function empty_message( $empty_message = '' ): self {
1085 $this->empty_message = $empty_message;
1086 return $this;
1087 }
1088
1089 /**
1090 * Set loading message for input.
1091 *
1092 * @since 4.0.0
1093 *
1094 * @param string $loading_message the empty message.
1095 *
1096 * @return self
1097 */
1098 public function loading_message( $loading_message = '' ): self {
1099 $this->loading_message = $loading_message;
1100 return $this;
1101 }
1102
1103 /**
1104 * Set selection time mode.
1105 *
1106 * @since 4.0.0
1107 *
1108 * @param int $mode Mode (12 or 24).
1109 *
1110 * @return self
1111 */
1112 public function selection_time_mode( $mode = 12 ): self {
1113 $this->selection_time_mode = $mode;
1114 return $this;
1115 }
1116
1117 /**
1118 * Set interval for time input options.
1119 *
1120 * @since 4.0.0
1121 *
1122 * @param int $interval Interval in minutes.
1123 *
1124 * @return self
1125 */
1126 public function interval( int $interval = 30 ): self {
1127 $this->interval = max( 1, $interval );
1128 return $this;
1129 }
1130
1131 /**
1132 * Set whether to show password strength meter.
1133 *
1134 * @since 4.0.0
1135 *
1136 * @param bool $show_strength Whether to show strength meter.
1137 *
1138 * @return self
1139 */
1140 public function show_strength( $show_strength = true ): self {
1141 $this->show_strength = $show_strength;
1142 return $this;
1143 }
1144
1145 /**
1146 * Set minimum password strength.
1147 *
1148 * @since 4.0.0
1149 *
1150 * @param int $min_strength Minimum strength score (0-5).
1151 *
1152 * @return self
1153 */
1154 public function min_strength( $min_strength = 3 ): self {
1155 $this->min_strength = $min_strength;
1156 return $this;
1157 }
1158
1159
1160
1161 /**
1162 * Options for select input field.
1163 *
1164 * @since 4.0.0
1165 *
1166 * @param array $options the options for input field.
1167 *
1168 * Example format for $options:
1169 * ```
1170 * $options = array(
1171 * array(
1172 * 'label' => '',
1173 * 'value' => '',
1174 * 'icon' => '',
1175 * 'description' => '',
1176 * )
1177 * );
1178 * ```.
1179 *
1180 * @return self
1181 */
1182 public function options( $options = array() ): self {
1183 $this->options = $options;
1184 return $this;
1185 }
1186
1187 /**
1188 * Set the number of max option to be selected.
1189 *
1190 * @since 4.0.0
1191 *
1192 * @param integer $max_selections the number of max selections.
1193 *
1194 * @return self
1195 */
1196 public function max_selections( $max_selections = 3 ): self {
1197 $this->max_selections = $max_selections;
1198 return $this;
1199 }
1200
1201 /**
1202 * Grouped Options for select input field.
1203 *
1204 * @since 4.0.0
1205 *
1206 * @param array $groups the options for input field.
1207 *
1208 * Example format for $groups:
1209 *
1210 * ```
1211 * $groups = array(
1212 * array(
1213 * 'label' => '',
1214 * 'options' => array(
1215 * array(
1216 * 'label' => '',
1217 * 'value' => '',
1218 * ),
1219 * array(
1220 * 'label' => '',
1221 * 'value' => '',
1222 * ),
1223 * array(
1224 * 'label' => '',
1225 * 'value' => '',
1226 * ),
1227 * ),
1228 * ),
1229 * );
1230 * ```.
1231 *
1232 * @return self
1233 */
1234 public function groups( $groups = array() ): self {
1235 $this->groups = $groups;
1236 return $this;
1237 }
1238
1239 /**
1240 * Render select input trigger button.
1241 *
1242 * @since 4.0.0
1243 *
1244 * @return string
1245 */
1246 protected function render_select_input_button(): string {
1247
1248 $single_input = '
1249 <div class="tutor-select-value">
1250 <template x-if="selectedOptions.length > 0 && selectedOptions[0].icon">
1251 <span class="tutor-select-value-icon" x-data="tutorIcon({ name: selectedOptions[0].icon })"></span>
1252 </template>
1253 <span
1254 class="tutor-select-value-text"
1255 :class="{ \'tutor-select-value-placeholder\': selectedValues.size === 0 }"
1256 x-text="displayValue"
1257 ></span>
1258 </div>';
1259
1260 $multiple_input = sprintf(
1261 '
1262 <div class="tutor-select-tags">
1263 <template x-if="selectedOptions.length === 0">
1264 <span class="tutor-select-value-placeholder" x-text="placeholder"></span>
1265 </template>
1266 <template x-for="option in selectedOptions" :key="option.value">
1267 <span class="tutor-select-tag">
1268 <span class="tutor-select-tag-label" x-text="option.label"></span>
1269 <button
1270 type="button"
1271 class="tutor-select-tag-remove"
1272 @click.stop="deselectOption(option, $event)"
1273 :aria-label="\'Remove \' + option.label"
1274 >
1275 %s
1276 </button>
1277 </span>
1278 </template>
1279 </div>
1280 ',
1281 SvgIcon::make()->name( Icon::CROSS )->size( 12 )->get()
1282 );
1283
1284 $right_icon_html = $this->right_icon ? $this->right_icon : SvgIcon::make()->name( Icon::CHEVRON_DOWN )->size( 16 )->get();
1285
1286 $input_button = $this->multiple ? $multiple_input : $single_input;
1287
1288 return sprintf(
1289 '<button
1290 type="button"
1291 class="tutor-select-trigger"
1292 data-select-trigger
1293 @click="toggle()"
1294 :aria-expanded="isOpen.toString()"
1295 :aria-haspopup="\'listbox\'"
1296 :disabled="disabled"
1297 >
1298 %s
1299 <div class="tutor-select-actions" x-cloak>
1300 <template x-if="canClear">
1301 <button
1302 type="button"
1303 class="tutor-select-clear"
1304 @click.stop="clear($event)"
1305 aria-label="%s"
1306 >
1307 %s
1308 </button>
1309 </template>
1310 <span class="tutor-select-arrow" :data-open="isOpen.toString()">%s</span>
1311 </div>
1312 </button>
1313 ',
1314 $input_button,
1315 esc_attr__( 'Clear selection', 'tutor' ),
1316 SvgIcon::make()->name( Icon::CROSS )->size( 16 )->get(),
1317 $right_icon_html
1318 );
1319 }
1320
1321 /**
1322 * Render option values for different selection.
1323 *
1324 * @since 4.0.0
1325 *
1326 * @param boolean $is_grouped whether the options are grouped.
1327 *
1328 * @return string
1329 */
1330 protected function render_selection_option( $is_grouped = false ): string {
1331 $option_type = $is_grouped ? 'group.options' : 'filteredOptions';
1332
1333 return sprintf(
1334 '
1335 <template x-for="(option, optionIndex) in %s" :key="option.value">
1336 <div
1337 class="tutor-select-option"
1338 data-select-option
1339 :data-disabled="option.disabled ? \'true\' : \'false\'"
1340 :data-selected="isSelected(option) ? \'true\' : \'false\'"
1341 :data-highlighted="isHighlighted(filteredOptions.indexOf(option)) ? \'true\' : \'false\'"
1342 @click="selectOption(option)"
1343 @mouseenter="highlightedIndex = filteredOptions.indexOf(option)"
1344 role="option"
1345 :aria-selected="isSelected(option).toString()"
1346 >
1347 <template x-if="option.icon">
1348 <span class="tutor-select-option-icon" x-data="tutorIcon({ name: option.icon })"></span>
1349 </template>
1350 <div class="tutor-select-option-content">
1351 <template x-if="option.html_label">
1352 <div class="tutor-select-option-label" x-html="option.html_label"></div>
1353 </template>
1354 <template x-if="!option.html_label">
1355 <div class="tutor-select-option-label" x-text="option.label"></div>
1356 </template>
1357 <template x-if="option.description">
1358 <div class="tutor-select-option-description" x-text="option.description"></div>
1359 </template>
1360 </div>
1361 </div>
1362 </template>
1363 ',
1364 $option_type
1365 );
1366 }
1367
1368 /**
1369 * Get grouped select option markup
1370 *
1371 * @since 4.0.0
1372 *
1373 * @return string
1374 */
1375 protected function get_grouped_option_markup(): string {
1376 return sprintf(
1377 '
1378 <template x-if="!isLoading && !loading && hasGroups">
1379 <template x-for="(group, groupIndex) in filteredGroups" :key="groupIndex">
1380 <div class="tutor-select-group">
1381 <div class="tutor-select-group-label" x-text="group.label"></div>
1382 <div class="tutor-select-group-options">
1383 %s
1384 </div>
1385 </div>
1386 </template>
1387 </template>',
1388 $this->render_selection_option( true )
1389 );
1390 }
1391
1392 /**
1393 * Get flat select option markup
1394 *
1395 * @since 4.0.0
1396 *
1397 * @return string
1398 */
1399 protected function get_flat_option_markup(): string {
1400 return sprintf(
1401 '
1402 <template x-if="!isLoading && !loading && !hasGroups">
1403 %s
1404 </template>
1405 ',
1406 $this->render_selection_option()
1407 );
1408 }
1409
1410 /**
1411 * Get select input search field markup
1412 *
1413 * @since 4.0.0
1414 *
1415 * @return string
1416 */
1417 protected function get_search_input_markup(): string {
1418
1419 $left_icon_html = $this->left_icon ? $this->left_icon : SvgIcon::make()->name( Icon::SEARCH_2 )->size( 20 )->get();
1420
1421 $search_input = sprintf(
1422 '
1423 <template x-if="searchable">
1424 <div class="tutor-select-search">
1425 <span class="tutor-select-search-icon">
1426 %s
1427 </span>
1428 <input
1429 type="text"
1430 class="tutor-select-search-input"
1431 data-select-search
1432 :placeholder="searchPlaceholder"
1433 x-model="searchQuery"
1434 @input="handleSearch($event.target.value)"
1435 @keydown.stop
1436 />
1437 </div>
1438 </div>
1439 </template>
1440 ',
1441 $left_icon_html
1442 );
1443
1444 return $search_input;
1445 }
1446
1447 /**
1448 * Render select input option list.
1449 *
1450 * @since 4.0.0
1451 *
1452 * @return string
1453 */
1454 protected function render_select_input_options(): string {
1455
1456 $loading_state = '
1457 <template x-if="isLoading || loading">
1458 <div class="tutor-select-loading">
1459 <span class="tutor-select-loading-spinner"></span>
1460 <span x-text="loadingMessage"></span>
1461 </div>
1462 </template>';
1463
1464 $empty_state = '
1465 <template x-if="!isLoading && !loading && filteredOptions.length === 0">
1466 <div class="tutor-select-empty" x-text="emptyMessage"></div>
1467 </template>';
1468
1469 $grouped_options = $this->get_grouped_option_markup();
1470 $flat_options = $this->get_flat_option_markup();
1471
1472 return sprintf(
1473 '<div
1474 x-show="isOpen"
1475 x-cloak
1476 x-transition
1477 @click.outside="close()"
1478 class="tutor-select-menu"
1479 data-select-menu
1480 :data-position="dropdownPosition"
1481 :style="{ maxHeight: maxHeight + \'px\' }"
1482 >
1483 %s
1484 <div class="tutor-select-options">
1485 %s
1486 %s
1487 %s
1488 %s
1489 </div>
1490 </div>',
1491 $this->get_search_input_markup(),
1492 $loading_state,
1493 $empty_state,
1494 $grouped_options,
1495 $flat_options
1496 );
1497 }
1498
1499 /**
1500 * Render the Select Input Component HTML.
1501 *
1502 * @since 4.0.0
1503 *
1504 * @return string
1505 */
1506 protected function render_select_input(): string {
1507 $props = array(
1508 'options' => $this->options,
1509 'groups' => $this->groups,
1510
1511 'searchable' => $this->searchable,
1512 'clearable' => $this->clearable,
1513 'disabled' => $this->disabled,
1514 'loading' => $this->loading,
1515
1516 'multiple' => $this->multiple,
1517
1518 'name' => $this->name,
1519 'required' => $this->required,
1520
1521 'placeholder' => $this->placeholder,
1522 'searchPlaceholder' => $this->search_placeholder,
1523 'emptyMessage' => $this->empty_message,
1524 'loadingMessage' => $this->loading_message,
1525 'maxHeight' => $this->max_height,
1526
1527 );
1528
1529 if ( $this->value ) {
1530 $props['value'] = $this->value;
1531 }
1532
1533 if ( $this->max_selections ) {
1534 $props['maxSelections'] = $this->max_selections;
1535 }
1536
1537 if ( null !== $this->close_on_select ) {
1538 $props['closeOnSelect'] = $this->close_on_select;
1539 }
1540
1541 $size_class = '';
1542 if ( Size::SM === $this->size ) {
1543 $size_class = 'tutor-select-sm';
1544 } elseif ( Size::LG === $this->size ) {
1545 $size_class = 'tutor-select-lg';
1546 }
1547
1548 $props_json_raw = wp_json_encode( $props );
1549 $props_json = $props_json_raw;
1550
1551 if ( null !== $this->on_change && '' !== $this->on_change ) {
1552 $props_json = sprintf( 'Object.assign(%s, { onChange: %s })', $props_json_raw, $this->on_change );
1553 }
1554
1555 $props_json = htmlspecialchars( $props_json, ENT_QUOTES, 'UTF-8' );
1556 $select_input_buttons = $this->render_select_input_button() ?? '';
1557 $select_input_options = $this->render_select_input_options() ?? '';
1558
1559 return sprintf(
1560 '<div
1561 x-data="tutorSelect(%s)"
1562 class="tutor-select %s"
1563 :data-disabled="disabled.toString()"
1564 %s
1565 >
1566 %s
1567 %s
1568 </div>',
1569 $props_json,
1570 $size_class,
1571 $this->get_attributes_string(),
1572 $select_input_buttons,
1573 $select_input_options
1574 );
1575 }
1576
1577 /**
1578 * Render password input.
1579 *
1580 * @since 4.0.0
1581 *
1582 * @return string Password HTML.
1583 */
1584 protected function render_password_input() {
1585 $input_id = ! empty( $this->id ) ? $this->id : $this->name;
1586
1587 $input_classes = 'tutor-input';
1588 if ( Size::SM === $this->size ) {
1589 $input_classes .= ' tutor-input-sm';
1590 } elseif ( Size::LG === $this->size ) {
1591 $input_classes .= ' tutor-input-lg';
1592 }
1593
1594 // Password inputs always have a right icon (toggle button).
1595 $input_classes .= ' tutor-input-content-right';
1596
1597 if ( ! empty( $this->left_icon ) ) {
1598 $input_classes .= ' tutor-input-content-left';
1599 }
1600
1601 $input_attrs = sprintf(
1602 'type="password" id="%s" name="%s" class="%s" %s',
1603 esc_attr( $input_id ),
1604 esc_attr( $this->name ),
1605 esc_attr( $input_classes ),
1606 $this->get_attributes_string()
1607 );
1608
1609 if ( ! empty( $this->placeholder ) ) {
1610 $input_attrs .= sprintf( ' placeholder="%s"', esc_attr( $this->placeholder ) );
1611 }
1612
1613 if ( ! empty( $this->value ) ) {
1614 $input_attrs .= sprintf( ' value="%s"', esc_attr( $this->value ) );
1615 }
1616
1617 if ( $this->disabled ) {
1618 $input_attrs .= ' disabled';
1619 }
1620
1621 $left_icon_html = '';
1622 if ( ! empty( $this->left_icon ) ) {
1623 $left_icon_html = sprintf(
1624 '<div class="tutor-input-content tutor-input-content-left">%s</div>',
1625 $this->left_icon
1626 );
1627 }
1628
1629 // Toggle button is always present for password fields.
1630 $toggle_button = '
1631 <button
1632 type="button"
1633 class="tutor-input-password-toggle"
1634 x-bind="getToggleBindings()"
1635 >
1636 <span x-show="!showPassword" x-cloak>' . SvgIcon::make()->name( Icon::EYE_OFF )->size( 16 )->get() . '</span>
1637 <span x-show="showPassword" x-cloak>' . SvgIcon::make()->name( Icon::EYE )->size( 16 )->get() . '</span>
1638 </button>
1639 ';
1640
1641 $right_icon_html = sprintf(
1642 '<div class="tutor-input-content tutor-input-content-right">%s</div>',
1643 $toggle_button
1644 );
1645
1646 return sprintf(
1647 '<div class="tutor-input-wrapper">
1648 <input %s>
1649 %s
1650 %s
1651 </div>',
1652 $input_attrs,
1653 $left_icon_html,
1654 $right_icon_html
1655 );
1656 }
1657
1658
1659 /**
1660 * Render text/email/password/number input.
1661 *
1662 * @since 4.0.0
1663 *
1664 * @return string InputField HTML.
1665 */
1666 protected function render_text_input() {
1667 $input_id = ! empty( $this->id ) ? $this->id : $this->name;
1668
1669 $input_classes = 'tutor-input';
1670
1671 if ( Size::SM === $this->size ) {
1672 $input_classes .= ' tutor-input-sm';
1673 } elseif ( Size::LG === $this->size ) {
1674 $input_classes .= ' tutor-input-lg';
1675 }
1676
1677 if ( ! empty( $this->left_icon ) ) {
1678 $input_classes .= ' tutor-input-content-left';
1679 }
1680 if ( ! empty( $this->right_icon ) ) {
1681 $input_classes .= ' tutor-input-content-right';
1682 }
1683 if ( ! $this->disabled && $this->clearable ) {
1684 $input_classes .= ' tutor-input-content-clear';
1685 }
1686
1687 $input_attrs = sprintf(
1688 'type="%s" id="%s" name="%s" class="%s" %s',
1689 esc_attr( $this->type ),
1690 esc_attr( $input_id ),
1691 esc_attr( $this->name ),
1692 esc_attr( $input_classes ),
1693 $this->get_attributes_string()
1694 );
1695
1696 if ( ! empty( $this->placeholder ) ) {
1697 $input_attrs .= sprintf( ' placeholder="%s"', esc_attr( $this->placeholder ) );
1698 }
1699
1700 if ( ! empty( $this->value ) ) {
1701 $input_attrs .= sprintf( ' value="%s"', esc_attr( $this->value ) );
1702 }
1703
1704 if ( $this->disabled ) {
1705 $input_attrs .= ' disabled';
1706 }
1707
1708 $left_icon_html = '';
1709 if ( ! empty( $this->left_icon ) ) {
1710 $left_icon_html = sprintf(
1711 '<div class="tutor-input-content tutor-input-content-left">%s</div>',
1712 $this->left_icon
1713 );
1714 }
1715
1716 $right_icon_html = '';
1717 if ( ! empty( $this->right_icon ) ) {
1718 $right_icon_html = sprintf(
1719 '<div class="tutor-input-content tutor-input-content-right">%s</div>',
1720 $this->right_icon
1721 );
1722 }
1723
1724 $clear_button_html = '';
1725 if ( ! $this->disabled && $this->clearable ) {
1726 $clear_icon = SvgIcon::make()->name( Icon::CROSS )->size( 16 )->get();
1727
1728 $clear_button_html = sprintf(
1729 '<button
1730 type="button"
1731 class="tutor-input-clear-button"
1732 aria-label="Clear input"
1733 x-cloak
1734 x-show="values[\'%1$s\'] && String(values[\'%1$s\']).length > 0"
1735 @click="setValue(\'%1$s\', \'\')"
1736 >%2$s</button>',
1737 esc_attr( $this->name ),
1738 $clear_icon
1739 );
1740 }
1741
1742 return sprintf(
1743 '<div class="tutor-input-wrapper">
1744 <input %s>
1745 %s
1746 %s
1747 %s
1748 </div>
1749 ',
1750 $input_attrs,
1751 $left_icon_html,
1752 $right_icon_html,
1753 $clear_button_html
1754 );
1755 }
1756
1757 /**
1758 * Render textarea input.
1759 *
1760 * @since 4.0.0
1761 *
1762 * @return string Textarea HTML.
1763 */
1764 protected function render_textarea() {
1765 $input_id = ! empty( $this->id ) ? $this->id : $this->name;
1766
1767 $input_classes = 'tutor-input tutor-text-area';
1768
1769 if ( Size::SM === $this->size ) {
1770 $input_classes .= ' tutor-input-sm';
1771 } elseif ( Size::LG === $this->size ) {
1772 $input_classes .= ' tutor-input-lg';
1773 }
1774
1775 if ( $this->clearable ) {
1776 $input_classes .= ' tutor-input-content-clear';
1777 }
1778
1779 $input_attrs = sprintf(
1780 'id="%s" name="%s" class="%s" %s',
1781 esc_attr( $input_id ),
1782 esc_attr( $this->name ),
1783 esc_attr( $input_classes ),
1784 $this->get_attributes_string()
1785 );
1786
1787 if ( ! empty( $this->placeholder ) ) {
1788 $input_attrs .= sprintf( ' placeholder="%s"', esc_attr( $this->placeholder ) );
1789 }
1790
1791 if ( $this->disabled ) {
1792 $input_attrs .= ' disabled';
1793 }
1794
1795 $clear_button_html = '';
1796 if ( $this->clearable ) {
1797 $clear_icon = SvgIcon::make()->name( Icon::CROSS )->size( 16 )->get();
1798 $clear_button_html = sprintf(
1799 '<button
1800 type="button"
1801 class="tutor-input-clear-button"
1802 aria-label="Clear input"
1803 x-cloak
1804 x-show="values[\'%1$s\'] && String(values[\'%1$s\']).length > 0"
1805 @click="setValue(\'%1$s\', \'\')"
1806 >%2$s</button>',
1807 esc_attr( $this->name ),
1808 $clear_icon
1809 );
1810 }
1811
1812 return sprintf(
1813 '<div class="tutor-input-wrapper">
1814 <textarea %s>%s</textarea>
1815 %s
1816 </div>',
1817 $input_attrs,
1818 esc_textarea( $this->value ),
1819 $clear_button_html,
1820 );
1821 }
1822
1823 /**
1824 * Render checkbox input.
1825 *
1826 * @since 4.0.0
1827 *
1828 * @return string Checkbox HTML.
1829 */
1830 protected function render_checkbox() {
1831 $input_id = ! empty( $this->id ) ? $this->id : $this->name;
1832
1833 $input_classes = 'tutor-checkbox';
1834 if ( 'md' === $this->size ) {
1835 $input_classes .= ' tutor-checkbox-md';
1836 }
1837 if ( $this->intermediate ) {
1838 $input_classes .= ' tutor-checkbox-intermediate';
1839 }
1840
1841 $input_attrs = sprintf(
1842 'type="checkbox" id="%s" name="%s" class="%s" %s',
1843 esc_attr( $input_id ),
1844 esc_attr( $this->name ),
1845 esc_attr( $input_classes ),
1846 $this->get_attributes_string()
1847 );
1848
1849 if ( ! empty( $this->value ) ) {
1850 $input_attrs .= sprintf( ' value="%s"', esc_attr( $this->value ) );
1851 }
1852
1853 if ( $this->checked ) {
1854 $input_attrs .= ' checked';
1855 }
1856
1857 if ( $this->disabled ) {
1858 $input_attrs .= ' disabled';
1859 }
1860
1861 return sprintf(
1862 '<div class="tutor-input-wrapper">
1863 <input %s>
1864 <label for="%s" class="tutor-label%s">%s</label>
1865 </div>',
1866 $input_attrs,
1867 esc_attr( $input_id ),
1868 $this->required ? ' tutor-label-required' : '',
1869 ! empty( $this->label_html ) ? $this->label_html : $this->esc( $this->label )
1870 );
1871 }
1872
1873 /**
1874 * Render radio input.
1875 *
1876 * @since 4.0.0
1877 *
1878 * @return string Radio HTML.
1879 */
1880 protected function render_radio() {
1881 $input_id = ! empty( $this->id ) ? $this->id : $this->name;
1882
1883 $input_classes = 'tutor-radio';
1884 if ( 'md' === $this->size ) {
1885 $input_classes .= ' tutor-radio-md';
1886 }
1887
1888 $input_attrs = sprintf(
1889 'type="radio" id="%s" name="%s" class="%s" %s',
1890 esc_attr( $input_id ),
1891 esc_attr( $this->name ),
1892 esc_attr( $input_classes ),
1893 $this->get_attributes_string()
1894 );
1895
1896 if ( ! empty( $this->value ) ) {
1897 $input_attrs .= sprintf( ' value="%s"', esc_attr( $this->value ) );
1898 }
1899
1900 if ( $this->checked ) {
1901 $input_attrs .= ' checked';
1902 }
1903
1904 if ( $this->disabled ) {
1905 $input_attrs .= ' disabled';
1906 }
1907
1908 return sprintf(
1909 '<div class="tutor-input-wrapper">
1910 <input %s>
1911 <label for="%s" class="tutor-label%s">%s</label>
1912 </div>',
1913 $input_attrs,
1914 esc_attr( $input_id ),
1915 $this->required ? ' tutor-label-required' : '',
1916 ! empty( $this->label_html ) ? $this->label_html : $this->esc( $this->label )
1917 );
1918 }
1919
1920 /**
1921 * Render switch input.
1922 *
1923 * @since 4.0.0
1924 *
1925 * @return string Switch HTML.
1926 */
1927 protected function render_switch() {
1928 $input_id = ! empty( $this->id ) ? $this->id : $this->name;
1929
1930 $input_classes = 'tutor-switch';
1931 if ( 'md' === $this->size ) {
1932 $input_classes .= ' tutor-switch-md';
1933 }
1934 if ( $this->intermediate ) {
1935 $input_classes .= ' tutor-switch--intermediate';
1936 }
1937
1938 $input_attrs = sprintf(
1939 'type="checkbox" id="%s" name="%s" class="%s" %s',
1940 esc_attr( $input_id ),
1941 esc_attr( $this->name ),
1942 esc_attr( $input_classes ),
1943 $this->get_attributes_string()
1944 );
1945
1946 if ( ! empty( $this->value ) ) {
1947 $input_attrs .= sprintf( ' value="%s"', esc_attr( $this->value ) );
1948 }
1949
1950 if ( $this->checked ) {
1951 $input_attrs .= ' checked';
1952 }
1953
1954 if ( $this->disabled ) {
1955 $input_attrs .= ' disabled';
1956 }
1957
1958 return sprintf(
1959 '<div class="tutor-input-wrapper">
1960 <input %s>
1961 <label for="%s" class="tutor-label%s">%s</label>
1962 </div>',
1963 $input_attrs,
1964 esc_attr( $input_id ),
1965 $this->required ? ' tutor-label-required' : '',
1966 $this->esc( $this->label )
1967 );
1968 }
1969
1970 /**
1971 * Render date/time input.
1972 *
1973 * @since 4.0.0
1974 *
1975 * @return string InputField HTML.
1976 */
1977 protected function render_date_input() {
1978 $original_type = $this->type;
1979 $original_left_icon = $this->left_icon;
1980 $this->type = 'text';
1981
1982 if ( empty( $this->left_icon ) && empty( $this->right_icon ) ) {
1983 $this->left_icon = SvgIcon::make()->name( Icon::CALENDAR_2 )->size( 20 )->get();
1984 }
1985
1986 $options = array(
1987 'inputMode' => true,
1988 );
1989
1990 if ( InputType::DATE_TIME === $original_type ) {
1991 $options['selectionTimeMode'] = $this->selection_time_mode ? $this->selection_time_mode : 12;
1992 } elseif ( ! is_null( $this->selection_time_mode ) ) {
1993 $options['selectionTimeMode'] = $this->selection_time_mode;
1994 }
1995
1996 $config = array(
1997 'options' => $options,
1998 );
1999
2000 $json_config = wp_json_encode( $config );
2001 $this->attr( 'x-data', "tutorCalendar($json_config)" );
2002 $this->attr( 'readonly', 'readonly' );
2003
2004 $html = $this->render_text_input();
2005
2006 $this->type = $original_type;
2007 $this->left_icon = $original_left_icon;
2008
2009 return $html;
2010 }
2011
2012 /**
2013 * Render time input.
2014 *
2015 * @since 4.0.0
2016 *
2017 * @return string InputField HTML.
2018 */
2019 protected function render_time_input() {
2020 $props = array(
2021 'name' => $this->name,
2022 'value' => $this->value,
2023 'placeholder' => $this->placeholder,
2024 'disabled' => $this->disabled,
2025 'clearable' => $this->clearable,
2026 'required' => $this->required,
2027 'interval' => $this->interval,
2028 );
2029
2030 $props_json = htmlspecialchars( wp_json_encode( $props ), ENT_QUOTES, 'UTF-8' );
2031 $input_icon = $this->left_icon;
2032 if ( empty( $input_icon ) ) {
2033 $input_icon = SvgIcon::make()->name( Icon::CLOCK )->size( 20 )->get();
2034 }
2035 $clear_icon = SvgIcon::make()->name( Icon::CROSS )->size( 12 )->get();
2036
2037 return sprintf(
2038 '<div
2039 x-data="tutorTimeInput(%1$s)"
2040 class="tutor-time-input"
2041 @click.outside="handleClickOutside()"
2042 %2$s
2043 >
2044 <div class="tutor-input-wrapper" x-ref="trigger">
2045 <input
2046 type="text"
2047 class="tutor-input tutor-input-content-left"
2048 :class="{ \'tutor-input-content-clear\': canClear }"
2049 :placeholder="placeholder"
2050 :disabled="disabled"
2051 :value="value"
2052 @click.stop="toggleDropdown()"
2053 @keydown="onInputKeydown($event)"
2054 @input="onInputChange($event)"
2055 autocomplete="off"
2056 aria-haspopup="listbox"
2057 :aria-expanded="open.toString()"
2058 :aria-disabled="disabled.toString()"
2059 />
2060
2061 <span class="tutor-input-content tutor-input-content-left" aria-hidden="true">
2062 %3$s
2063 </span>
2064
2065 <button
2066 type="button"
2067 class="tutor-input-clear-button"
2068 x-show="canClear"
2069 @click.stop="clearValue()"
2070 aria-label="%4$s"
2071 >
2072 %5$s
2073 </button>
2074 </div>
2075
2076 <div
2077 x-ref="content"
2078 class="tutor-time-input-menu"
2079 x-show="open"
2080 x-cloak
2081 x-transition.opacity.scale.origin.top
2082 @keydown="onListKeydown($event)"
2083 >
2084 <template x-for="(option, index) in options" :key="option">
2085 <button
2086 type="button"
2087 class="tutor-time-input-option"
2088 :data-option-index="index"
2089 :data-active="(highlightedIndex === index).toString()"
2090 :data-selected="(value === option).toString()"
2091 @click="selectOption(option)"
2092 @mousemove="highlightedIndex = index"
2093 @focus="highlightedIndex = index"
2094 x-text="option"
2095 ></button>
2096 </template>
2097 </div>
2098 </div>',
2099 $props_json,
2100 $this->get_attributes_string(),
2101 $input_icon,
2102 esc_attr__( 'Clear time', 'tutor' ),
2103 $clear_icon
2104 );
2105 }
2106
2107
2108
2109 /**
2110 * Get the input field HTML.
2111 *
2112 * @since 4.0.0
2113 *
2114 * @return string HTML output.
2115 */
2116 public function get(): string {
2117 if ( empty( $this->name ) ) {
2118 return '';
2119 }
2120
2121 $input_id = ! empty( $this->id ) ? $this->id : $this->name;
2122
2123 // Field wrapper classes.
2124 $field_classes = array( 'tutor-input-field' );
2125 if ( ! empty( $this->error ) ) {
2126 $field_classes[] = ' tutor-input-field-error';
2127 }
2128
2129 if ( isset( $this->attributes['class'] ) ) {
2130 $field_classes[] = $this->attributes['class'];
2131 }
2132
2133 // Render label for text inputs.
2134 $label_html = '';
2135 if ( ! in_array( $this->type, array( InputType::CHECKBOX, InputType::RADIO, InputType::SWITCH ), true ) && ! empty( $this->label ) ) {
2136 $label_html = sprintf(
2137 '<label for="%s" class="tutor-label%s">%s</label>',
2138 esc_attr( $input_id ),
2139 $this->required ? ' tutor-label-required' : '',
2140 $this->esc( $this->label )
2141 );
2142 }
2143
2144 // Render input based on type.
2145 $input_html = '';
2146 $root_attrs = '';
2147 switch ( $this->type ) {
2148 case InputType::PASSWORD:
2149 $input_html = $this->render_password_input();
2150
2151 $password_props = array(
2152 'showStrength' => $this->show_strength,
2153 'minStrength' => $this->min_strength,
2154 );
2155 $alpine_data = sprintf( 'tutorPasswordInput(%s)', htmlspecialchars( wp_json_encode( $password_props ), ENT_QUOTES, 'UTF-8' ) );
2156 $root_attrs = sprintf( ' x-data="%s"', $alpine_data );
2157 break;
2158 case InputType::TEXTAREA:
2159 $input_html = $this->render_textarea();
2160 break;
2161 case InputType::CHECKBOX:
2162 $input_html = $this->render_checkbox();
2163 break;
2164 case InputType::RADIO:
2165 $input_html = $this->render_radio();
2166 break;
2167 case InputType::SWITCH:
2168 $input_html = $this->render_switch();
2169 break;
2170 case InputType::DATE:
2171 case InputType::DATE_TIME:
2172 $input_html = $this->render_date_input();
2173 break;
2174 case InputType::TIME:
2175 $input_html = $this->render_time_input();
2176 break;
2177 case InputType::SELECT:
2178 $input_html = $this->render_select_input();
2179 break;
2180
2181 default:
2182 $input_html = $this->render_text_input();
2183 break;
2184 }
2185
2186 $error_html = sprintf(
2187 '<div
2188 class="tutor-error-text"
2189 x-cloak
2190 x-show="errors[\'%1$s\']"
2191 x-text="errors?.[\'%1$s\']?.message"
2192 role="alert"
2193 aria-live="polite"
2194 ></div>',
2195 esc_attr( $this->name )
2196 );
2197
2198 $help_html = sprintf(
2199 '<div
2200 class="tutor-help-text"
2201 x-show="!errors?.[\'%1$s\']?.message"
2202 >%2$s</div>',
2203 esc_attr( $this->name ),
2204 $this->help_text
2205 );
2206
2207 $strength_meter_html = '';
2208 if ( InputType::PASSWORD === $this->type && $this->show_strength ) {
2209 $strength_meter_html = sprintf(
2210 '<div
2211 class="tutor-help-text"
2212 x-show="password.length > 0"
2213 x-cloak
2214 x-bind="getStrengthTextBindings()"
2215 ></div>'
2216 );
2217 }
2218
2219 $this->component_string = sprintf(
2220 '<div class="%s" :class="{ \'tutor-input-field-error\': errors[\'%s\'] }" %s>%s%s%s%s%s</div>',
2221 esc_attr( implode( ' ', $field_classes ) ),
2222 esc_attr( $this->name ),
2223 $root_attrs,
2224 $label_html,
2225 $input_html,
2226 $error_html,
2227 $help_html,
2228 $strength_meter_html
2229 );
2230
2231 return $this->component_string;
2232 }
2233 }
2234