HEX
Server: LiteSpeed
System: Linux s787.bom1.mysecurecloudhost.com 4.18.0-477.13.1.lve.el8.x86_64 #1 SMP Thu Jun 1 16:40:47 EDT 2023 x86_64
User: mobilech (5348)
PHP: 8.1.34
Disabled: NONE
Upload Files
File: /home/mobilech/alliancelaptoptraining.com.np/wp-content/plugins/tutor/components/InputField.php
<?php
/**
 * Tutor Component: InputField
 *
 * Provides a fluent builder for rendering various input field types with
 * labels, validation states, and helper text.
 *
 * @package Tutor\Components
 * @author Themeum
 * @link https://themeum.com
 * @since 4.0.0
 */

namespace Tutor\Components;

defined( 'ABSPATH' ) || exit;

use TUTOR\Icon;
use ReflectionClass;
use Tutor\Components\Constants\Size;
use Tutor\Components\Constants\InputType;

/**
 * InputField Component Class.
 *
 * Example usage:
 * ```
 * // Text input with clear button
 * InputField::make()
 *     ->type( 'text' )
 *     ->name( 'full_name' )
 *     ->label( 'Full Name' )
 *     ->placeholder( 'Enter your full name' )
 *     ->required()
 *     ->clearable()
 *     ->help_text( 'This is a helper text.' )
 *     ->attr( 'x-bind', "register('full_name', { required: 'Name is required', minLength: { value: 2, message: 'Name must be at least 2 characters' } })")
 *     ->render();
 *
 * // Text input with left icon
 * InputField::make()
 *     ->type( 'text' )
 *     ->name( 'email' )
 *     ->label( 'Email' )
 *     ->left_icon( '<svg>...</svg>' )
 *     ->render();
 *
 * // Textarea
 * InputField::make()
 *     ->type( 'textarea' )
 *     ->name( 'bio' )
 *     ->label( 'Bio' )
 *     ->render();
 *
 * // Checkbox
 * InputField::make()
 *     ->type( 'checkbox' )
 *     ->name( 'agree' )
 *     ->label( 'I agree to terms' )
 *     ->size( 'md' )
 *     ->render();
 *
 * // Radio
 * InputField::make()
 *     ->type( 'radio' )
 *     ->name( 'gender' )
 *     ->label( 'Male' )
 *     ->value( 'male' )
 *     ->render();
 *
 * // Switch
 * InputField::make()
 *     ->type( 'switch' )
 *     ->name( 'notifications' )
 *     ->label( 'Enable notifications?' )
 *     ->size( 'md' )
 *     ->render();
 *
 * // InputField with error
 * InputField::make()
 *     ->type( 'text' )
 *     ->name( 'username' )
 *     ->label( 'Username' )
 *     ->error( 'This field is required.' )
 *     ->render();
 *
 * // Select
 * InputField::make()
 *      ->type( 'select' )
 *      ->name( 'interests' )
 *      ->label( 'Interests' )
 *      ->placeholder( 'Select your interests')
 *      ->required( 'Please select an interest')
 *      ->clearable()
 *      ->options( $interests )
 *      ->multiple()
 *      ->searchable()
 *      ->size( 'md' )
 *      ->max_selections( 2 )
 *      ->help_text( 'This is a helper next.' )
 *      ->render();
 *
 * ```
 *
 * @since 4.0.0
 */
class InputField extends BaseComponent {

	/**
	 * InputField type (text|email|password|number|textarea|checkbox|radio|switch|time).
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $type = InputType::TEXT;

	/**
	 * InputField name attribute.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $name = '';

	/**
	 * InputField ID attribute.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $id = '';

	/**
	 * InputField value.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $value = '';

	/**
	 * InputField label text.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $label = '';

	/**
	 * InputField label HTML (for checkbox/radio labels that contain HTML markup).
	 * When set, this takes precedence over $label for checkbox/radio/switch types.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $label_html = '';

	/**
	 * InputField placeholder text.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $placeholder = '';

	/**
	 * Search Input placeholder text.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $search_placeholder = '';

	/**
	 * Help text below input.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $help_text = '';

	/**
	 * Error message text.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $error = '';

	/**
	 * Whether input is required.
	 *
	 * @since 4.0.0
	 *
	 * @var bool
	 */
	protected $required = false;

	/**
	 * Input field attr like alpine attribute for validation
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $attr = '';

	/**
	 * Whether input is disabled.
	 *
	 * @since 4.0.0
	 *
	 * @var bool
	 */
	protected $disabled = false;

	/**
	 * Whether input is loading
	 *
	 * @since 4.0.0
	 *
	 * @var boolean
	 */
	protected $loading = false;

	/**
	 * Whether input is checked (checkbox/radio/switch).
	 *
	 * @since 4.0.0
	 *
	 * @var bool
	 */
	protected $checked = false;

	/**
	 * Whether checkbox is intermediate state.
	 *
	 * @since 4.0.0
	 *
	 * @var bool
	 */
	protected $intermediate = false;

	/**
	 * Whether to use WordPress media library instead of native file input.
	 *
	 * @since 4.0.0
	 *
	 * @var bool
	 */
	protected $use_wp_media = false;

	/**
	 * WordPress media modal title.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $wp_media_title = '';

	/**
	 * WordPress media modal button text.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $wp_media_button_text = '';

	/**
	 * WordPress media library type filter.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $wp_media_library_type = '';

	/**
	 * Component variant.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $variant = '';

	/**
	 * Whether input has clear button.
	 *
	 * @since 4.0.0
	 *
	 * @var bool
	 */
	protected $clearable = false;

	/**
	 * Whether input is searchable.
	 *
	 * @since 4.0.0
	 *
	 * @var boolean
	 */
	protected $searchable = false;

	/**
	 * Whether multiple option can be selected in input.
	 *
	 * @since 4.0.0
	 *
	 * @var boolean
	 */
	protected $multiple = false;

	/**
	 * Left icon SVG markup.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $left_icon = '';

	/**
	 * Right icon SVG markup.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $right_icon = '';

	/**
	 * InputField size (sm|md).
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $size = Size::MD;

	/**
	 * Options for select input field.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $options = array();



	/**
	 * Grouped option for input field.
	 *
	 * @since 4.0.0
	 *
	 * @var array
	 */
	protected $groups = array();

	/**
	 * Max number of input option to be selected.
	 *
	 * @since 4.0.0
	 *
	 * @var integer
	 */
	protected $max_selections = 0;

	/**
	 * Close select input on selecting option.
	 *
	 * @since 4.0.0
	 *
	 * @var boolean
	 */
	protected $close_on_select = null;

	/**
	 * Raw JS onChange callback for select input.
	 *
	 * @since 4.0.0
	 *
	 * @var string|null
	 */
	protected $on_change = null;

	/**
	 * Max Height for select input.
	 *
	 * @since 4.0.0
	 *
	 * @var integer
	 */
	protected $max_height = 280;

	/**
	 * Empty state message for input.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $empty_message = '';

	/**
	 * Loading state message for input.
	 *
	 * @since 4.0.0
	 *
	 * @var string
	 */
	protected $loading_message = '';

	/**
	 * Selection time mode (12|24).
	 *
	 * @since 4.0.0
	 *
	 * @var int|null
	 */
	protected $selection_time_mode = null;

	/**
	 * Time input option interval in minutes.
	 *
	 * @since 4.0.0
	 *
	 * @var int
	 */
	protected $interval = 30;

	/**
	 * Whether to show password strength meter.
	 *
	 * @since 4.0.0
	 *
	 * @var bool
	 */
	protected $show_strength = false;

	/**
	 * Minimum password strength required.
	 *
	 * @since 4.0.0
	 *
	 * @var int
	 */
	protected $min_strength = 3;

	/**
	 * Set input type.
	 *
	 * @since 4.0.0
	 *
	 * @param string $type InputField type.
	 *
	 * @return $this
	 */
	public function type( $type ) {
		$allowed = $this->get_allowed_types();
		if ( in_array( $type, $allowed, true ) ) {
			$this->type = $type;
		}
		return $this;
	}

	/**
	 * Get allowed input types
	 *
	 * @since 4.0.0
	 *
	 * @return array
	 */
	public function get_allowed_types() {
		$class     = new ReflectionClass( InputType::class );
		$constants = $class->getConstants();

		return array_values( $constants );
	}

	/**
	 * Set input name.
	 *
	 * @since 4.0.0
	 *
	 * @param string $name InputField name.
	 *
	 * @return $this
	 */
	public function name( $name ) {
		$this->name = sanitize_text_field( $name );
		return $this;
	}

	/**
	 * Set input ID.
	 *
	 * @since 4.0.0
	 *
	 * @param string $id InputField ID.
	 *
	 * @return $this
	 */
	public function id( $id ) {
		$this->id = sanitize_text_field( $id );
		return $this;
	}

	/**
	 * Set input value.
	 *
	 * @since 4.0.0
	 *
	 * @param string $value InputField value.
	 *
	 * @return $this
	 */
	public function value( $value ) {
		$this->value = $value;
		return $this;
	}

	/**
	 * Set input label.
	 *
	 * @since 4.0.0
	 *
	 * @param string $label Label text.
	 *
	 * @return $this
	 */
	public function label( $label ) {
		$this->label = $label;
		return $this;
	}

	/**
	 * Set input label as HTML markup (for checkbox/radio/switch labels containing links or other HTML).
	 * Content is sanitized via wp_kses with a safe set of allowed tags.
	 *
	 * @since 4.0.0
	 *
	 * @param string $html        Raw HTML label content.
	 * @param array  $allowed_tags Optional. Allowed HTML tags. Defaults to a, span, strong, em.
	 *
	 * @return $this
	 */
	public function label_html( $html, $allowed_tags = array() ) {
		if ( empty( $allowed_tags ) ) {
			$allowed_tags = array(
				'a'      => array(
					'href'   => true,
					'target' => true,
					'class'  => true,
					'rel'    => true,
				),
				'span'   => array( 'class' => true ),
				'strong' => array(),
				'em'     => array(),
			);
		}
		$this->label_html = wp_kses( $html, $allowed_tags );
		return $this;
	}

	/**
	 * Set input placeholder.
	 *
	 * @since 4.0.0
	 *
	 * @param string $placeholder Placeholder text.
	 * @param bool   $search whether placeholder is for search input.
	 *
	 * @return $this
	 */
	public function placeholder( $placeholder, $search = false ) {
		if ( $search ) {
			$this->search_placeholder = $placeholder;
		} else {
			$this->placeholder = $placeholder;
		}
		return $this;
	}

	/**
	 * Set the help text with HTML sanitization.
	 *
	 * @since 4.0.0
	 *
	 * @param string $text Help text.
	 * @param array  $allowed_tags Optional. Allowed HTML tags and attributes for wp_kses().
	 *
	 * @return $this
	 */
	public function help_text( $text, $allowed_tags = array() ) {

		if ( ! empty( $allowed_tags ) && is_array( $allowed_tags ) ) {
			$this->help_text = wp_kses( $text, $allowed_tags );
			return $this;
		}

		$this->help_text = esc_html( $text );
		return $this;
	}

	/**
	 * Set the max height for select input
	 *
	 * @since 4.0.0
	 *
	 * @param integer $max_height the max height.
	 *
	 * @return self
	 */
	public function max_height( $max_height = 280 ): self {
		$this->max_height = $max_height;
		return $this;
	}

	/**
	 * Collapse select input on selecting option.
	 *
	 * @since 4.0.0
	 *
	 * @param boolean $close_on_select close on selecting option.
	 *
	 * @return self
	 */
	public function collapsable( $close_on_select = true ): self {
		$this->close_on_select = $close_on_select;
		return $this;
	}

	/**
	 * Set raw JS onChange callback for select input.
	 *
	 * Example: ->on_change( 'function(value){ console.log(value); }' )
	 *
	 * @since 4.0.0
	 *
	 * @param string $on_change Raw JS function string.
	 *
	 * @return self
	 */
	public function on_change( $on_change ): self {
		$this->on_change = $on_change;
		return $this;
	}

	/**
	 * Set error message.
	 *
	 * @since 4.0.0
	 *
	 * @param string $error Error message.
	 *
	 * @return $this
	 */
	public function error( $error ) {
		$this->error = $error;
		return $this;
	}

	/**
	 * Set required state.
	 *
	 * @since 4.0.0
	 *
	 * @param bool|string $required Whether input is required.
	 *
	 * @return $this
	 */
	public function required( $required = true ) {
		$this->required = $required;
		return $this;
	}

	/**
	 * Set disabled state.
	 *
	 * @since 4.0.0
	 *
	 * @param bool $disabled Whether input is disabled.
	 *
	 * @return $this
	 */
	public function disabled( $disabled = true ) {
		$this->disabled = (bool) $disabled;
		return $this;
	}

	/**
	 * Set loading state.
	 *
	 * @since 4.0.0
	 *
	 * @param bool $loading Whether input is loading.
	 *
	 * @return $this
	 */
	public function loading( $loading = true ) {
		$this->loading = (bool) $loading;
		return $this;
	}

	/**
	 * Set checked state.
	 *
	 * @since 4.0.0
	 *
	 * @param bool $checked Whether input is checked.
	 *
	 * @return $this
	 */
	public function checked( $checked = true ) {
		$this->checked = (bool) $checked;
		return $this;
	}

	/**
	 * Set intermediate state for checkbox.
	 *
	 * @since 4.0.0
	 *
	 * @param bool $intermediate Whether checkbox is intermediate.
	 *
	 * @return $this
	 */
	public function intermediate( $intermediate = true ) {
		$this->intermediate = $intermediate;

		return $this;
	}

	/**
	 * Enable WordPress media library instead of native file input.
	 *
	 * @since 4.0.0
	 *
	 * @param bool $use_wp_media Whether to use WordPress media library.
	 *
	 * @return $this
	 */
	public function use_wp_media( $use_wp_media = true ) {
		$this->use_wp_media = (bool) $use_wp_media;

		return $this;
	}

	/**
	 * Set WordPress media modal title.
	 *
	 * @since 4.0.0
	 *
	 * @param string $title Modal title.
	 *
	 * @return $this
	 */
	public function wp_media_title( $title ) {
		$this->wp_media_title = $title;

		return $this;
	}

	/**
	 * Set WordPress media modal button text.
	 *
	 * @since 4.0.0
	 *
	 * @param string $text Button text.
	 *
	 * @return $this
	 */
	public function wp_media_button_text( $text ) {
		$this->wp_media_button_text = $text;

		return $this;
	}

	/**
	 * Set WordPress media library type filter.
	 *
	 * @since 4.0.0
	 *
	 * @param string $type Library type (image, video, audio, application).
	 *
	 * @return $this
	 */
	public function wp_media_library_type( $type ) {
		$this->wp_media_library_type = $type;

		return $this;
	}

	/**
	 * Set component variant.
	 *
	 * @since 4.0.0
	 *
	 * @param string $variant Component variant.
	 *
	 * @return $this
	 */
	public function variant( $variant ) {
		$this->variant = $variant;

		return $this;
	}

	/**
	 * Enable clear button.
	 *
	 * @since 4.0.0
	 *
	 * @param bool $clearable Whether to show clear button.
	 *
	 * @return $this
	 */
	public function clearable( $clearable = true ) {
		$this->clearable = (bool) $clearable;
		return $this;
	}

	/**
	 * Set left icon.
	 *
	 * @since 4.0.0
	 *
	 * @param string $icon SVG icon markup.
	 *
	 * @return $this
	 */
	public function left_icon( $icon ) {
		$this->left_icon = $icon;
		return $this;
	}

	/**
	 * Set right icon.
	 *
	 * @since 4.0.0
	 *
	 * @param string $icon SVG icon markup.
	 *
	 * @return $this
	 */
	public function right_icon( $icon ) {
		$this->right_icon = $icon;
		return $this;
	}

	/**
	 * Set input size.
	 *
	 * @since 4.0.0
	 *
	 * @param string $size Size (sm|md).
	 *
	 * @return $this
	 */
	public function size( $size ) {
		$allowed = array( 'sm', 'md', 'lg' );
		if ( in_array( $size, $allowed, true ) ) {
			$this->size = $size;
		}
		return $this;
	}

	/**
	 * Set whether multiple select enabled or not.
	 *
	 * @since 4.0.0
	 *
	 * @param boolean $multiple whether multiple select is enable.
	 *
	 * @return self
	 */
	public function multiple( $multiple = true ): self {
		$this->multiple = $multiple;
		return $this;
	}

	/**
	 * Set if input field is searchable.
	 *
	 * @since 4.0.0
	 *
	 * @param boolean $searchable whether input is searchable.
	 *
	 * @return self
	 */
	public function searchable( $searchable = true ): self {
		$this->searchable = $searchable;
		return $this;
	}

	/**
	 * Set empty message for input.
	 *
	 * @since 4.0.0
	 *
	 * @param string $empty_message the empty message.
	 *
	 * @return self
	 */
	public function empty_message( $empty_message = '' ): self {
		$this->empty_message = $empty_message;
		return $this;
	}

	/**
	 * Set loading message for input.
	 *
	 * @since 4.0.0
	 *
	 * @param string $loading_message the empty message.
	 *
	 * @return self
	 */
	public function loading_message( $loading_message = '' ): self {
		$this->loading_message = $loading_message;
		return $this;
	}

	/**
	 * Set selection time mode.
	 *
	 * @since 4.0.0
	 *
	 * @param int $mode Mode (12 or 24).
	 *
	 * @return self
	 */
	public function selection_time_mode( $mode = 12 ): self {
		$this->selection_time_mode = $mode;
		return $this;
	}

	/**
	 * Set interval for time input options.
	 *
	 * @since 4.0.0
	 *
	 * @param int $interval Interval in minutes.
	 *
	 * @return self
	 */
	public function interval( int $interval = 30 ): self {
		$this->interval = max( 1, $interval );
		return $this;
	}

	/**
	 * Set whether to show password strength meter.
	 *
	 * @since 4.0.0
	 *
	 * @param bool $show_strength Whether to show strength meter.
	 *
	 * @return self
	 */
	public function show_strength( $show_strength = true ): self {
		$this->show_strength = $show_strength;
		return $this;
	}

	/**
	 * Set minimum password strength.
	 *
	 * @since 4.0.0
	 *
	 * @param int $min_strength Minimum strength score (0-5).
	 *
	 * @return self
	 */
	public function min_strength( $min_strength = 3 ): self {
		$this->min_strength = $min_strength;
		return $this;
	}



	/**
	 * Options for select input field.
	 *
	 * @since 4.0.0
	 *
	 * @param array $options the options for input field.
	 *
	 * Example format for $options:
	 * ```
	 * $options = array(
	 *    array(
	 *      'label'       => '',
	 *      'value'       => '',
	 *      'icon'        => '',
	 *      'description' => '',
	 *    )
	 * );
	 * ```.
	 *
	 * @return self
	 */
	public function options( $options = array() ): self {
		$this->options = $options;
		return $this;
	}

	/**
	 * Set the number of max option to be selected.
	 *
	 * @since 4.0.0
	 *
	 * @param integer $max_selections the number of max selections.
	 *
	 * @return self
	 */
	public function max_selections( $max_selections = 3 ): self {
		$this->max_selections = $max_selections;
		return $this;
	}

	/**
	 * Grouped Options for select input field.
	 *
	 * @since 4.0.0
	 *
	 * @param array $groups the options for input field.
	 *
	 * Example format for $groups:
	 *
	 * ```
	 * $groups = array(
	 *    array(
	 *       'label'   => '',
	 *       'options' => array(
	 *           array(
	 *               'label' => '',
	 *               'value' => '',
	 *           ),
	 *           array(
	 *               'label' => '',
	 *               'value' => '',
	 *           ),
	 *           array(
	 *               'label' => '',
	 *               'value' => '',
	 *           ),
	 *       ),
	 *   ),
	 * );
	 * ```.
	 *
	 * @return self
	 */
	public function groups( $groups = array() ): self {
		$this->groups = $groups;
		return $this;
	}

	/**
	 * Render select input trigger button.
	 *
	 * @since 4.0.0
	 *
	 * @return string
	 */
	protected function render_select_input_button(): string {

		$single_input = '
			<div class="tutor-select-value">
				<template x-if="selectedOptions.length > 0 && selectedOptions[0].icon">
					<span class="tutor-select-value-icon" x-data="tutorIcon({ name: selectedOptions[0].icon })"></span>
				</template>
				<span 
					class="tutor-select-value-text"
					:class="{ \'tutor-select-value-placeholder\': selectedValues.size === 0 }"
					x-text="displayValue"
				></span>
			</div>';

		$multiple_input = sprintf(
			'
			<div class="tutor-select-tags">
				<template x-if="selectedOptions.length === 0">
					<span class="tutor-select-value-placeholder" x-text="placeholder"></span>
				</template>
				<template x-for="option in selectedOptions" :key="option.value">
					<span class="tutor-select-tag">
						<span class="tutor-select-tag-label" x-text="option.label"></span>
						<button
							type="button"
							class="tutor-select-tag-remove"
							@click.stop="deselectOption(option, $event)"
							:aria-label="\'Remove \' + option.label"
						>
						%s
						</button>
					</span>
				</template>
			</div>
		',
			SvgIcon::make()->name( Icon::CROSS )->size( 12 )->get()
		);

		$right_icon_html = $this->right_icon ? $this->right_icon : SvgIcon::make()->name( Icon::CHEVRON_DOWN )->size( 16 )->get();

		$input_button = $this->multiple ? $multiple_input : $single_input;

		return sprintf(
			'<button
				type="button"
				class="tutor-select-trigger"
				data-select-trigger
				@click="toggle()"
				:aria-expanded="isOpen.toString()"
				:aria-haspopup="\'listbox\'"
				:disabled="disabled"
			>
			%s
				<div class="tutor-select-actions" x-cloak>
					<template x-if="canClear">
						<button
							type="button"
							class="tutor-select-clear"
							@click.stop="clear($event)"
							aria-label="%s"
						>
						%s
						</button>
					</template>
					<span class="tutor-select-arrow" :data-open="isOpen.toString()">%s</span>
				</div>
			</button>
			',
			$input_button,
			esc_attr__( 'Clear selection', 'tutor' ),
			SvgIcon::make()->name( Icon::CROSS )->size( 16 )->get(),
			$right_icon_html
		);
	}

	/**
	 * Render option values for different selection.
	 *
	 * @since 4.0.0
	 *
	 * @param boolean $is_grouped whether the options are grouped.
	 *
	 * @return string
	 */
	protected function render_selection_option( $is_grouped = false ): string {
		$option_type = $is_grouped ? 'group.options' : 'filteredOptions';

		return sprintf(
			'
			<template x-for="(option, optionIndex) in %s" :key="option.value">
				<div
					class="tutor-select-option"
					data-select-option
					:data-disabled="option.disabled ? \'true\' : \'false\'"
					:data-selected="isSelected(option) ? \'true\' : \'false\'"
					:data-highlighted="isHighlighted(filteredOptions.indexOf(option)) ? \'true\' : \'false\'"
					@click="selectOption(option)"
					@mouseenter="highlightedIndex = filteredOptions.indexOf(option)"
					role="option"
					:aria-selected="isSelected(option).toString()"
				>
					<template x-if="option.icon">
						<span class="tutor-select-option-icon" x-data="tutorIcon({ name: option.icon })"></span>
					</template>
					<div class="tutor-select-option-content">
						<template x-if="option.html_label">
							<div class="tutor-select-option-label" x-html="option.html_label"></div>
						</template>
						<template x-if="!option.html_label">
							<div class="tutor-select-option-label" x-text="option.label"></div>
						</template>
						<template x-if="option.description">
							<div class="tutor-select-option-description" x-text="option.description"></div>
						</template>
					</div>
				</div>
			</template>
			',
			$option_type
		);
	}

	/**
	 * Get grouped select option markup
	 *
	 * @since 4.0.0
	 *
	 * @return string
	 */
	protected function get_grouped_option_markup(): string {
		return sprintf(
			'
		<template x-if="!isLoading && !loading && hasGroups">
			<template x-for="(group, groupIndex) in filteredGroups" :key="groupIndex">
				<div class="tutor-select-group">
					<div class="tutor-select-group-label" x-text="group.label"></div>
					<div class="tutor-select-group-options">
						%s
					</div>
				</div>
			</template>
		</template>',
			$this->render_selection_option( true )
		);
	}

	/**
	 * Get flat select option markup
	 *
	 * @since 4.0.0
	 *
	 * @return string
	 */
	protected function get_flat_option_markup(): string {
		return sprintf(
			'
			<template x-if="!isLoading && !loading && !hasGroups">
				%s
			</template>
			',
			$this->render_selection_option()
		);
	}

	/**
	 * Get select input search field markup
	 *
	 * @since 4.0.0
	 *
	 * @return string
	 */
	protected function get_search_input_markup(): string {

		$left_icon_html = $this->left_icon ? $this->left_icon : SvgIcon::make()->name( Icon::SEARCH_2 )->size( 20 )->get();

		$search_input = sprintf(
			'
			<template x-if="searchable">
				<div class="tutor-select-search">
					<span class="tutor-select-search-icon">
							%s
					</span>
					<input
							type="text"
							class="tutor-select-search-input"
							data-select-search
							:placeholder="searchPlaceholder"
							x-model="searchQuery"
							@input="handleSearch($event.target.value)"
							@keydown.stop
						/>
					</div>
				</div>
			</template>
			',
			$left_icon_html
		);

		return $search_input;
	}

	/**
	 * Render select input option list.
	 *
	 * @since 4.0.0
	 *
	 * @return string
	 */
	protected function render_select_input_options(): string {

		$loading_state = '
			<template x-if="isLoading || loading">
				<div class="tutor-select-loading">
					<span class="tutor-select-loading-spinner"></span>
					<span x-text="loadingMessage"></span>
				</div>
			</template>';

		$empty_state = '
			<template x-if="!isLoading && !loading && filteredOptions.length === 0">
				<div class="tutor-select-empty" x-text="emptyMessage"></div>
			</template>';

		$grouped_options = $this->get_grouped_option_markup();
		$flat_options    = $this->get_flat_option_markup();

		return sprintf(
			'<div
				x-show="isOpen"
				x-cloak
				x-transition
				@click.outside="close()"
				class="tutor-select-menu"
				data-select-menu
				:data-position="dropdownPosition"
				:style="{ maxHeight: maxHeight + \'px\' }"
			>
			%s
				<div class="tutor-select-options">
					%s
					%s
					%s
					%s
				</div>
			</div>',
			$this->get_search_input_markup(),
			$loading_state,
			$empty_state,
			$grouped_options,
			$flat_options
		);
	}

	/**
	 * Render the Select Input Component HTML.
	 *
	 * @since 4.0.0
	 *
	 * @return string
	 */
	protected function render_select_input(): string {
		$props = array(
			'options'           => $this->options,
			'groups'            => $this->groups,

			'searchable'        => $this->searchable,
			'clearable'         => $this->clearable,
			'disabled'          => $this->disabled,
			'loading'           => $this->loading,

			'multiple'          => $this->multiple,

			'name'              => $this->name,
			'required'          => $this->required,

			'placeholder'       => $this->placeholder,
			'searchPlaceholder' => $this->search_placeholder,
			'emptyMessage'      => $this->empty_message,
			'loadingMessage'    => $this->loading_message,
			'maxHeight'         => $this->max_height,

		);

		if ( $this->value ) {
			$props['value'] = $this->value;
		}

		if ( $this->max_selections ) {
			$props['maxSelections'] = $this->max_selections;
		}

		if ( null !== $this->close_on_select ) {
			$props['closeOnSelect'] = $this->close_on_select;
		}

		$size_class = '';
		if ( Size::SM === $this->size ) {
			$size_class = 'tutor-select-sm';
		} elseif ( Size::LG === $this->size ) {
			$size_class = 'tutor-select-lg';
		}

		$props_json_raw = wp_json_encode( $props );
		$props_json     = $props_json_raw;

		if ( null !== $this->on_change && '' !== $this->on_change ) {
			$props_json = sprintf( 'Object.assign(%s, { onChange: %s })', $props_json_raw, $this->on_change );
		}

		$props_json           = htmlspecialchars( $props_json, ENT_QUOTES, 'UTF-8' );
		$select_input_buttons = $this->render_select_input_button() ?? '';
		$select_input_options = $this->render_select_input_options() ?? '';

		return sprintf(
			'<div
				x-data="tutorSelect(%s)"
				class="tutor-select %s"
				:data-disabled="disabled.toString()"
				%s
			>
				%s
				%s
			</div>',
			$props_json,
			$size_class,
			$this->get_attributes_string(),
			$select_input_buttons,
			$select_input_options
		);
	}

	/**
	 * Render password input.
	 *
	 * @since 4.0.0
	 *
	 * @return string Password HTML.
	 */
	protected function render_password_input() {
		$input_id = ! empty( $this->id ) ? $this->id : $this->name;

		$input_classes = 'tutor-input';
		if ( Size::SM === $this->size ) {
			$input_classes .= ' tutor-input-sm';
		} elseif ( Size::LG === $this->size ) {
			$input_classes .= ' tutor-input-lg';
		}

		// Password inputs always have a right icon (toggle button).
		$input_classes .= ' tutor-input-content-right';

		if ( ! empty( $this->left_icon ) ) {
			$input_classes .= ' tutor-input-content-left';
		}

		$input_attrs = sprintf(
			'type="password" id="%s" name="%s" class="%s" %s',
			esc_attr( $input_id ),
			esc_attr( $this->name ),
			esc_attr( $input_classes ),
			$this->get_attributes_string()
		);

		if ( ! empty( $this->placeholder ) ) {
			$input_attrs .= sprintf( ' placeholder="%s"', esc_attr( $this->placeholder ) );
		}

		if ( ! empty( $this->value ) ) {
			$input_attrs .= sprintf( ' value="%s"', esc_attr( $this->value ) );
		}

		if ( $this->disabled ) {
			$input_attrs .= ' disabled';
		}

		$left_icon_html = '';
		if ( ! empty( $this->left_icon ) ) {
			$left_icon_html = sprintf(
				'<div class="tutor-input-content tutor-input-content-left">%s</div>',
				$this->left_icon
			);
		}

		// Toggle button is always present for password fields.
		$toggle_button = '
			<button 
				type="button" 
				class="tutor-input-password-toggle"
				x-bind="getToggleBindings()"
			>
				<span x-show="!showPassword" x-cloak>' . SvgIcon::make()->name( Icon::EYE_OFF )->size( 16 )->get() . '</span>
				<span x-show="showPassword" x-cloak>' . SvgIcon::make()->name( Icon::EYE )->size( 16 )->get() . '</span>
			</button>
		';

		$right_icon_html = sprintf(
			'<div class="tutor-input-content tutor-input-content-right">%s</div>',
			$toggle_button
		);

		return sprintf(
			'<div class="tutor-input-wrapper">
				<input %s>
				%s
				%s
			</div>',
			$input_attrs,
			$left_icon_html,
			$right_icon_html
		);
	}


	/**
	 * Render text/email/password/number input.
	 *
	 * @since 4.0.0
	 *
	 * @return string InputField HTML.
	 */
	protected function render_text_input() {
		$input_id = ! empty( $this->id ) ? $this->id : $this->name;

		$input_classes = 'tutor-input';

		if ( Size::SM === $this->size ) {
			$input_classes .= ' tutor-input-sm';
		} elseif ( Size::LG === $this->size ) {
			$input_classes .= ' tutor-input-lg';
		}

		if ( ! empty( $this->left_icon ) ) {
			$input_classes .= ' tutor-input-content-left';
		}
		if ( ! empty( $this->right_icon ) ) {
			$input_classes .= ' tutor-input-content-right';
		}
		if ( ! $this->disabled && $this->clearable ) {
			$input_classes .= ' tutor-input-content-clear';
		}

		$input_attrs = sprintf(
			'type="%s" id="%s" name="%s" class="%s" %s',
			esc_attr( $this->type ),
			esc_attr( $input_id ),
			esc_attr( $this->name ),
			esc_attr( $input_classes ),
			$this->get_attributes_string()
		);

		if ( ! empty( $this->placeholder ) ) {
			$input_attrs .= sprintf( ' placeholder="%s"', esc_attr( $this->placeholder ) );
		}

		if ( ! empty( $this->value ) ) {
			$input_attrs .= sprintf( ' value="%s"', esc_attr( $this->value ) );
		}

		if ( $this->disabled ) {
			$input_attrs .= ' disabled';
		}

		$left_icon_html = '';
		if ( ! empty( $this->left_icon ) ) {
			$left_icon_html = sprintf(
				'<div class="tutor-input-content tutor-input-content-left">%s</div>',
				$this->left_icon
			);
		}

		$right_icon_html = '';
		if ( ! empty( $this->right_icon ) ) {
			$right_icon_html = sprintf(
				'<div class="tutor-input-content tutor-input-content-right">%s</div>',
				$this->right_icon
			);
		}

		$clear_button_html = '';
		if ( ! $this->disabled && $this->clearable ) {
			$clear_icon = SvgIcon::make()->name( Icon::CROSS )->size( 16 )->get();

			$clear_button_html = sprintf(
				'<button 
					type="button"
					class="tutor-input-clear-button"
					aria-label="Clear input"
					x-cloak
					x-show="values[\'%1$s\'] && String(values[\'%1$s\']).length > 0"
					@click="setValue(\'%1$s\', \'\')"
				>%2$s</button>',
				esc_attr( $this->name ),
				$clear_icon
			);
		}

		return sprintf(
			'<div class="tutor-input-wrapper">
				<input %s>
				%s
				%s
				%s
			</div>
			',
			$input_attrs,
			$left_icon_html,
			$right_icon_html,
			$clear_button_html
		);
	}

	/**
	 * Render textarea input.
	 *
	 * @since 4.0.0
	 *
	 * @return string Textarea HTML.
	 */
	protected function render_textarea() {
		$input_id = ! empty( $this->id ) ? $this->id : $this->name;

		$input_classes = 'tutor-input tutor-text-area';

		if ( Size::SM === $this->size ) {
			$input_classes .= ' tutor-input-sm';
		} elseif ( Size::LG === $this->size ) {
			$input_classes .= ' tutor-input-lg';
		}

		if ( $this->clearable ) {
			$input_classes .= ' tutor-input-content-clear';
		}

		$input_attrs = sprintf(
			'id="%s" name="%s" class="%s" %s',
			esc_attr( $input_id ),
			esc_attr( $this->name ),
			esc_attr( $input_classes ),
			$this->get_attributes_string()
		);

		if ( ! empty( $this->placeholder ) ) {
			$input_attrs .= sprintf( ' placeholder="%s"', esc_attr( $this->placeholder ) );
		}

		if ( $this->disabled ) {
			$input_attrs .= ' disabled';
		}

		$clear_button_html = '';
		if ( $this->clearable ) {
			$clear_icon        = SvgIcon::make()->name( Icon::CROSS )->size( 16 )->get();
			$clear_button_html = sprintf(
				'<button 
					type="button" 
					class="tutor-input-clear-button" 
					aria-label="Clear input"
					x-cloak
					x-show="values[\'%1$s\'] && String(values[\'%1$s\']).length > 0"
					@click="setValue(\'%1$s\', \'\')"
				>%2$s</button>',
				esc_attr( $this->name ),
				$clear_icon
			);
		}

		return sprintf(
			'<div class="tutor-input-wrapper">
				<textarea %s>%s</textarea>
				%s
			</div>',
			$input_attrs,
			esc_textarea( $this->value ),
			$clear_button_html,
		);
	}

	/**
	 * Render checkbox input.
	 *
	 * @since 4.0.0
	 *
	 * @return string Checkbox HTML.
	 */
	protected function render_checkbox() {
		$input_id = ! empty( $this->id ) ? $this->id : $this->name;

		$input_classes = 'tutor-checkbox';
		if ( 'md' === $this->size ) {
			$input_classes .= ' tutor-checkbox-md';
		}
		if ( $this->intermediate ) {
			$input_classes .= ' tutor-checkbox-intermediate';
		}

		$input_attrs = sprintf(
			'type="checkbox" id="%s" name="%s" class="%s" %s',
			esc_attr( $input_id ),
			esc_attr( $this->name ),
			esc_attr( $input_classes ),
			$this->get_attributes_string()
		);

		if ( ! empty( $this->value ) ) {
			$input_attrs .= sprintf( ' value="%s"', esc_attr( $this->value ) );
		}

		if ( $this->checked ) {
			$input_attrs .= ' checked';
		}

		if ( $this->disabled ) {
			$input_attrs .= ' disabled';
		}

		return sprintf(
			'<div class="tutor-input-wrapper">
				<input %s>
				<label for="%s" class="tutor-label%s">%s</label>
			</div>',
			$input_attrs,
			esc_attr( $input_id ),
			$this->required ? ' tutor-label-required' : '',
			! empty( $this->label_html ) ? $this->label_html : $this->esc( $this->label )
		);
	}

	/**
	 * Render radio input.
	 *
	 * @since 4.0.0
	 *
	 * @return string Radio HTML.
	 */
	protected function render_radio() {
		$input_id = ! empty( $this->id ) ? $this->id : $this->name;

		$input_classes = 'tutor-radio';
		if ( 'md' === $this->size ) {
			$input_classes .= ' tutor-radio-md';
		}

		$input_attrs = sprintf(
			'type="radio" id="%s" name="%s" class="%s" %s',
			esc_attr( $input_id ),
			esc_attr( $this->name ),
			esc_attr( $input_classes ),
			$this->get_attributes_string()
		);

		if ( ! empty( $this->value ) ) {
			$input_attrs .= sprintf( ' value="%s"', esc_attr( $this->value ) );
		}

		if ( $this->checked ) {
			$input_attrs .= ' checked';
		}

		if ( $this->disabled ) {
			$input_attrs .= ' disabled';
		}

		return sprintf(
			'<div class="tutor-input-wrapper">
				<input %s>
				<label for="%s" class="tutor-label%s">%s</label>
			</div>',
			$input_attrs,
			esc_attr( $input_id ),
			$this->required ? ' tutor-label-required' : '',
			! empty( $this->label_html ) ? $this->label_html : $this->esc( $this->label )
		);
	}

	/**
	 * Render switch input.
	 *
	 * @since 4.0.0
	 *
	 * @return string Switch HTML.
	 */
	protected function render_switch() {
		$input_id = ! empty( $this->id ) ? $this->id : $this->name;

		$input_classes = 'tutor-switch';
		if ( 'md' === $this->size ) {
			$input_classes .= ' tutor-switch-md';
		}
		if ( $this->intermediate ) {
			$input_classes .= ' tutor-switch--intermediate';
		}

		$input_attrs = sprintf(
			'type="checkbox" id="%s" name="%s" class="%s" %s',
			esc_attr( $input_id ),
			esc_attr( $this->name ),
			esc_attr( $input_classes ),
			$this->get_attributes_string()
		);

		if ( ! empty( $this->value ) ) {
			$input_attrs .= sprintf( ' value="%s"', esc_attr( $this->value ) );
		}

		if ( $this->checked ) {
			$input_attrs .= ' checked';
		}

		if ( $this->disabled ) {
			$input_attrs .= ' disabled';
		}

		return sprintf(
			'<div class="tutor-input-wrapper">
				<input %s>
				<label for="%s" class="tutor-label%s">%s</label>
			</div>',
			$input_attrs,
			esc_attr( $input_id ),
			$this->required ? ' tutor-label-required' : '',
			$this->esc( $this->label )
		);
	}

	/**
	 * Render date/time input.
	 *
	 * @since 4.0.0
	 *
	 * @return string InputField HTML.
	 */
	protected function render_date_input() {
		$original_type      = $this->type;
		$original_left_icon = $this->left_icon;
		$this->type         = 'text';

		if ( empty( $this->left_icon ) && empty( $this->right_icon ) ) {
			$this->left_icon = SvgIcon::make()->name( Icon::CALENDAR_2 )->size( 20 )->get();
		}

		$options = array(
			'inputMode' => true,
		);

		if ( InputType::DATE_TIME === $original_type ) {
			$options['selectionTimeMode'] = $this->selection_time_mode ? $this->selection_time_mode : 12;
		} elseif ( ! is_null( $this->selection_time_mode ) ) {
			$options['selectionTimeMode'] = $this->selection_time_mode;
		}

		$config = array(
			'options' => $options,
		);

		$json_config = wp_json_encode( $config );
		$this->attr( 'x-data', "tutorCalendar($json_config)" );
		$this->attr( 'readonly', 'readonly' );

		$html = $this->render_text_input();

		$this->type      = $original_type;
		$this->left_icon = $original_left_icon;

		return $html;
	}

	/**
	 * Render time input.
	 *
	 * @since 4.0.0
	 *
	 * @return string InputField HTML.
	 */
	protected function render_time_input() {
		$props = array(
			'name'        => $this->name,
			'value'       => $this->value,
			'placeholder' => $this->placeholder,
			'disabled'    => $this->disabled,
			'clearable'   => $this->clearable,
			'required'    => $this->required,
			'interval'    => $this->interval,
		);

		$props_json = htmlspecialchars( wp_json_encode( $props ), ENT_QUOTES, 'UTF-8' );
		$input_icon = $this->left_icon;
		if ( empty( $input_icon ) ) {
			$input_icon = SvgIcon::make()->name( Icon::CLOCK )->size( 20 )->get();
		}
		$clear_icon = SvgIcon::make()->name( Icon::CROSS )->size( 12 )->get();

		return sprintf(
			'<div
				x-data="tutorTimeInput(%1$s)"
				class="tutor-time-input"
				@click.outside="handleClickOutside()"
				%2$s
			>
				<div class="tutor-input-wrapper" x-ref="trigger">
					<input
						type="text"
						class="tutor-input tutor-input-content-left"
						:class="{ \'tutor-input-content-clear\': canClear }"
						:placeholder="placeholder"
						:disabled="disabled"
						:value="value"
						@click.stop="toggleDropdown()"
						@keydown="onInputKeydown($event)"
						@input="onInputChange($event)"
						autocomplete="off"
						aria-haspopup="listbox"
						:aria-expanded="open.toString()"
						:aria-disabled="disabled.toString()"
					/>

					<span class="tutor-input-content tutor-input-content-left" aria-hidden="true">
						%3$s
					</span>

					<button
						type="button"
						class="tutor-input-clear-button"
						x-show="canClear"
						@click.stop="clearValue()"
						aria-label="%4$s"
					>
						%5$s
					</button>
				</div>

				<div
					x-ref="content"
					class="tutor-time-input-menu"
					x-show="open"
					x-cloak
					x-transition.opacity.scale.origin.top
					@keydown="onListKeydown($event)"
				>
					<template x-for="(option, index) in options" :key="option">
						<button
							type="button"
							class="tutor-time-input-option"
							:data-option-index="index"
							:data-active="(highlightedIndex === index).toString()"
							:data-selected="(value === option).toString()"
							@click="selectOption(option)"
							@mousemove="highlightedIndex = index"
							@focus="highlightedIndex = index"
							x-text="option"
						></button>
					</template>
				</div>
			</div>',
			$props_json,
			$this->get_attributes_string(),
			$input_icon,
			esc_attr__( 'Clear time', 'tutor' ),
			$clear_icon
		);
	}



	/**
	 * Get the input field HTML.
	 *
	 * @since 4.0.0
	 *
	 * @return string HTML output.
	 */
	public function get(): string {
		if ( empty( $this->name ) ) {
			return '';
		}

		$input_id = ! empty( $this->id ) ? $this->id : $this->name;

		// Field wrapper classes.
		$field_classes = array( 'tutor-input-field' );
		if ( ! empty( $this->error ) ) {
			$field_classes[] = ' tutor-input-field-error';
		}

		if ( isset( $this->attributes['class'] ) ) {
			$field_classes[] = $this->attributes['class'];
		}

		// Render label for text inputs.
		$label_html = '';
		if ( ! in_array( $this->type, array( InputType::CHECKBOX, InputType::RADIO, InputType::SWITCH ), true ) && ! empty( $this->label ) ) {
			$label_html = sprintf(
				'<label for="%s" class="tutor-label%s">%s</label>',
				esc_attr( $input_id ),
				$this->required ? ' tutor-label-required' : '',
				$this->esc( $this->label )
			);
		}

		// Render input based on type.
		$input_html = '';
		$root_attrs = '';
		switch ( $this->type ) {
			case InputType::PASSWORD:
				$input_html = $this->render_password_input();

				$password_props = array(
					'showStrength' => $this->show_strength,
					'minStrength'  => $this->min_strength,
				);
				$alpine_data    = sprintf( 'tutorPasswordInput(%s)', htmlspecialchars( wp_json_encode( $password_props ), ENT_QUOTES, 'UTF-8' ) );
				$root_attrs     = sprintf( ' x-data="%s"', $alpine_data );
				break;
			case InputType::TEXTAREA:
				$input_html = $this->render_textarea();
				break;
			case InputType::CHECKBOX:
				$input_html = $this->render_checkbox();
				break;
			case InputType::RADIO:
				$input_html = $this->render_radio();
				break;
			case InputType::SWITCH:
				$input_html = $this->render_switch();
				break;
			case InputType::DATE:
			case InputType::DATE_TIME:
				$input_html = $this->render_date_input();
				break;
			case InputType::TIME:
				$input_html = $this->render_time_input();
				break;
			case InputType::SELECT:
				$input_html = $this->render_select_input();
				break;

			default:
				$input_html = $this->render_text_input();
				break;
		}

		$error_html = sprintf(
			'<div 
				class="tutor-error-text" 
				x-cloak 
				x-show="errors[\'%1$s\']" 
				x-text="errors?.[\'%1$s\']?.message" 
				role="alert" 
				aria-live="polite"
			></div>',
			esc_attr( $this->name )
		);

		$help_html = sprintf(
			'<div
				class="tutor-help-text"
				x-show="!errors?.[\'%1$s\']?.message"
			>%2$s</div>',
			esc_attr( $this->name ),
			$this->help_text
		);

		$strength_meter_html = '';
		if ( InputType::PASSWORD === $this->type && $this->show_strength ) {
			$strength_meter_html = sprintf(
				'<div 
					class="tutor-help-text" 
					x-show="password.length > 0" 
					x-cloak 
					x-bind="getStrengthTextBindings()"
				></div>'
			);
		}

		$this->component_string = sprintf(
			'<div class="%s" :class="{ \'tutor-input-field-error\': errors[\'%s\'] }" %s>%s%s%s%s%s</div>',
			esc_attr( implode( ' ', $field_classes ) ),
			esc_attr( $this->name ),
			$root_attrs,
			$label_html,
			$input_html,
			$error_html,
			$help_html,
			$strength_meter_html
		);

		return $this->component_string;
	}
}