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/DropdownFilter.php
<?php
/**
 * DropdownFilter Component Class.
 *
 * @package Tutor\Components
 * @author Themeum
 * @link https://themeum.com
 * @since 4.0.0
 */

namespace Tutor\Components;

use Tutor\Components\Constants\Size;
use Tutor\Components\Constants\Variant;
use Tutor\Components\Constants\Positions;
use Tutor\Components\Constants\Color;
use Tutor\Components\Popover;
use TUTOR\Icon;
use TUTOR\Input;

defined( 'ABSPATH' ) || exit;

/**
 * Class DropdownFilter
 *
 * Example Usage:
 * ```
 * // Basic dropdown with custom options
 * DropdownFilter::make()
 *     ->options( $options )
 *     ->query_param( 'status' )
 *     ->variant( Variant::PRIMARY )
 *     ->size( Size::SMALL )
 *     ->search( true )
 *     ->render();
 *
 * // Course filter (convenience method)
 * DropdownFilter::make()
 *     ->options( $courses )
 *     ->count( $total_items )
 *     ->query_param( 'course_id' )
 *     ->variant( Variant::PRIMARY_SOFT )
 *     ->size( Size::X_SMALL )
 *     ->popover_size( Size::MEDIUM )
 *     ->position( Positions::BOTTOM_END )
 *     ->placeholder( __( 'Search Course', 'tutor' ) )
 *     ->render();
 * ```
 *
 * @since 4.0.0
 */
class DropdownFilter extends BaseComponent {

	/**
	 * Options for the dropdown
	 *
	 * @var array
	 */
	protected $options = array();

	/**
	 * Whether to show search box
	 *
	 * @var bool
	 */
	protected $show_search = false;

	/**
	 * Trigger button variant
	 *
	 * @var string
	 */
	protected $variant = Variant::LINK;

	/**
	 * Trigger button size
	 *
	 * @var string
	 */
	protected $size = Size::MEDIUM;

	/**
	 * Custom button CSS classes
	 *
	 * @var string
	 */
	protected $button_class = '';

	/**
	 * Search placeholder
	 *
	 * @var string
	 */
	protected $placeholder = '';

	/**
	 * Query parameter name to manage URL and active state
	 *
	 * @var string
	 */
	protected $query_param = '';

	/**
	 * Count to display
	 *
	 * @var int|null
	 */
	protected $count = null;

	/**
	 * Popover size
	 *
	 * @var string
	 */
	protected $popover_size = Size::SMALL;

	/**
	 * On change callback function name
	 *
	 * @var string
	 */
	protected $on_change = '';

	/**
	 * Alpine.js variable name for active state
	 *
	 * @var string
	 */
	protected $active_value = '';

	/**
	 * Base URL for building filter links (preserves other query params when set).
	 *
	 * @var string
	 */
	protected $base_url = '';


	/**
	 * Icon size
	 *
	 * @var int|null
	 */
	protected $icon_size = null;

	/**
	 * Popover placement position
	 *
	 * @var string
	 */
	protected $position = Positions::BOTTOM_START;

	/**
	 * Set options list
	 *
	 * @param array $options list of options. Each item: array( 'label' => string, 'url' => string, 'count' => int|null, 'active' => bool, 'value' => mixed ).
	 *
	 * @return self
	 */
	public function options( array $options ): self {
		$this->options = $options;
		return $this;
	}

	/**
	 * Set query parameter name
	 *
	 * @param string $key query parameter name.
	 *
	 * @return self
	 */
	public function query_param( string $key ): self {
		$this->query_param = $key;
		return $this;
	}

	/**
	 * Set search visibility
	 *
	 * @param bool $show whether to show search.
	 *
	 * @return self
	 */
	public function search( bool $show ): self {
		$this->show_search = $show;
		return $this;
	}

	/**
	 * Set button variant
	 *
	 * @param string $variant button variant.
	 *
	 * @return self
	 */
	public function variant( string $variant ): self {
		$this->variant = $variant;
		return $this;
	}

	/**
	 * Set button size
	 *
	 * @param string $size button size.
	 *
	 * @return self
	 */
	public function size( string $size ): self {
		$this->size = $size;
		return $this;
	}

	/**
	 * Set button CSS classes
	 *
	 * @param string $classes CSS classes for the button.
	 *
	 * @return self
	 */
	public function button_class( string $classes ): self {
		$this->button_class = $classes;
		return $this;
	}

	/**
	 * Set search placeholder
	 *
	 * @param string $placeholder search placeholder.
	 *
	 * @return self
	 */
	public function placeholder( string $placeholder ): self {
		$this->placeholder = $placeholder;
		return $this;
	}

	/**
	 * Set popover width
	 *
	 * @param string $size popover width size (Size::SMALL, Size::MEDIUM, Size::LARGE).
	 *
	 * @return self
	 */
	public function popover_size( string $size ): self {
		$this->popover_size = $size;
		return $this;
	}

	/**
	 * Set icon size
	 *
	 * @param int $size icon size.
	 *
	 * @return self
	 */
	public function icon_size( int $size ): self {
		$this->icon_size = $size;
		return $this;
	}

	/**
	 * Set popover placement position.
	 *
	 * @param string $position placement position (Positions::TOP, Positions::BOTTOM, etc).
	 *
	 * @return self
	 */
	public function position( string $position ): self {
		$this->position = $position;
		return $this;
	}

	/**
	 * Get popover width in pixels based on size constant
	 *
	 * @param string $size Size constant.
	 *
	 * @return string Width in pixels.
	 */
	protected function get_popover_width( string $size ): string {
		switch ( $size ) {
			case Size::MEDIUM:
				return '275px';
			case Size::LARGE:
				return '350px';
			case Size::SMALL:
			default:
				return '172px';
		}
	}

	/**
	 * Set count
	 *
	 * @param int $count count to display.
	 *
	 * @return self
	 */
	public function count( int $count ): self {
		$this->count = $count;
		return $this;
	}

	/**
	 * Set JS callback on change
	 *
	 * @param string $method_name JS method name.
	 *
	 * @return self
	 */
	public function on_change( string $method_name ): self {
		$this->on_change = $method_name;
		return $this;
	}

	/**
	 * Set Alpine.js active state binding
	 *
	 * @param string $active_value Alpine variable name.
	 *
	 * @return self
	 */
	public function bind_active_value( string $active_value ): self {
		$this->active_value = $active_value;
		return $this;
	}

	/**
	 * Set base URL for filter links so other query params are preserved (cumulative filtering).
	 *
	 * @param string $url Full current page URL including query string.
	 *
	 * @return self
	 */
	public function base_url( string $url ): self {
		$this->base_url = $url;
		return $this;
	}

	/**
	 * Get the base URL used for auto-generated filter links.
	 *
	 * Removes transient pagination state so changing filters resets pagination.
	 *
	 * @return string
	 */
	protected function get_filter_base_url(): string {
		if ( ! empty( $this->base_url ) ) {
			return remove_query_arg( 'current_page', $this->base_url );
		}

		return remove_query_arg( 'current_page' );
	}


	/**
	 * Get component content
	 *
	 * @return string
	 */
	public function get(): string {
		$options = $this->options;
		if ( empty( $options ) ) {
			return '';
		}

		// Automatically manage active state and URL if query_param is set.
		if ( ! empty( $this->query_param ) ) {
			$current_val = Input::get( $this->query_param, '' );
			$base        = $this->get_filter_base_url();
			foreach ( $options as &$option ) {
				$val = isset( $option['value'] ) ? $option['value'] : '';
				if ( ! isset( $option['active'] ) ) {
					$option['active'] = (string) $val === (string) $current_val;
				}
				if ( ! isset( $option['url'] ) ) {
					if ( ! empty( $base ) ) {
						$option['url'] = ! empty( $val ) ? add_query_arg( $this->query_param, $val, $base ) : remove_query_arg( $this->query_param, $base );
					} else {
						$option['url'] = ! empty( $val ) ? add_query_arg( $this->query_param, $val ) : remove_query_arg( $this->query_param );
					}
				}
			}
			unset( $option );
		}

		$active_option = null;

		foreach ( $options as $option ) {
			if ( ! empty( $option['active'] ) ) {
				$active_option = $option;
				break;
			}
		}

		// Fallback to first option if none is active.
		if ( ! $active_option && ! empty( $options ) ) {
			$active_option = $options[0];
		}

		$label = $active_option['label'] ?? '';
		$count = $active_option['count'] ?? null;

		// If count is set via count() method, use it for the active option.
		if ( null !== $this->count ) {
			$count = $this->count;
		}

		$btn_class = $this->button_class;
		if ( empty( $btn_class ) ) {
			$size_class = 'tutor-btn-medium';
			if ( Size::SMALL === $this->size ) {
				$size_class = 'tutor-btn-small';
			} elseif ( Size::X_SMALL === $this->size ) {
				$size_class = 'tutor-btn-x-small';
			} elseif ( Size::LARGE === $this->size ) {
				$size_class = 'tutor-btn-large';
			}

			if ( Variant::PRIMARY === $this->variant ) {
				$btn_class = 'tutor-btn tutor-btn-primary ' . $size_class;
			} elseif ( Variant::PRIMARY_SOFT === $this->variant ) {
				$btn_class = 'tutor-btn tutor-btn-primary-soft ' . $size_class;
			} elseif ( Variant::OUTLINE === $this->variant ) {
				$btn_class = 'tutor-btn tutor-btn-outline ' . $size_class;
			} else {
				$btn_class = 'tutor-btn tutor-btn-link';
			}
		}

		$origin = Popover::TRANSFORM_ORIGIN_MAP[ $this->position ] ?? 'left.top';

		ob_start();
		?>
		<div
			class="tutor-dropdown-filter"
			x-data="{
				...tutorPopover({
					placement: '<?php echo esc_js( $this->position ); ?>',
					offset: 4,
				}),
				<?php if ( $this->active_value ) : ?>
					labels: <?php echo esc_attr( wp_json_encode( array_column( $options, 'label', 'value' ) ) ); ?>,
					counts: <?php echo esc_attr( wp_json_encode( array_column( $options, 'count', 'value' ) ) ); ?>
				<?php endif; ?>
			}"
		>
			<button 
				type="button" 
				x-ref="trigger" 
				@click="toggle()" 
				class="<?php echo esc_attr( $btn_class ); ?>"
			>
				<span class="tutor-truncate <?php echo Variant::LINK === $this->variant ? esc_attr( 'tutor-text-secondary' ) : ''; ?>" style="max-width: 150px;"
					<?php if ( $this->active_value ) : ?>
						x-text="labels[<?php echo esc_attr( $this->active_value ); ?>] || '<?php echo esc_js( $label ); ?>'"
					<?php endif; ?>
				>
					<?php echo esc_html( $label ); ?>
				</span>
				<?php if ( null !== $count || $this->active_value ) : ?>
					<span class="tutor-font-medium <?php echo Variant::LINK === $this->variant ? esc_attr( 'tutor-text-primary' ) : ''; ?>"
						<?php if ( $this->active_value ) : ?>
							x-text="'(' + (counts[<?php echo esc_attr( $this->active_value ); ?>] ?? '0') + ')'"
							x-show="counts[<?php echo esc_attr( $this->active_value ); ?>] !== null"
						<?php endif; ?>
					>
						<?php echo null !== $count ? '(' . esc_html( $count ) . ')' : ''; ?>
					</span>
				<?php endif; ?>
				<?php
				$icon_size = $this->icon_size;
				if ( null === $icon_size ) {
					$icon_size = in_array( $this->variant, array( Variant::PRIMARY, Variant::PRIMARY_SOFT ), true ) ? 16 : 20;
				}
				SvgIcon::make()->name( Icon::CHEVRON_DOWN_2 )->size( $icon_size )->color( Variant::PRIMARY_SOFT === $this->variant ? Color::BRAND : Color::SECONDARY )->render();
				?>
			</button>

			<div 
				x-ref="content"
				x-show="open"
				x-cloak
				x-transition.<?php echo esc_attr( $origin ); ?>
				@click.outside="handleClickOutside()"
				class="tutor-popover"
				style="width: <?php echo esc_attr( $this->get_popover_width( $this->popover_size ) ); ?>; max-width: unset;"
			>
				<div class="tutor-popover-menu" x-data="{ search: '', get hasResults() { return this.search === '' || [...$el.querySelectorAll('.tutor-popover-menu-item')].some(el => el.style.display !== 'none' && !el.hasAttribute('hidden')); } }">
					<?php if ( $this->show_search ) : ?>
						<div class="tutor-input-field tutor-px-5 tutor-pt-2 tutor-pb-4">
							<div class="tutor-input-wrapper">
								<div class="tutor-input-content tutor-input-content-left">
									<?php SvgIcon::make()->name( Icon::SEARCH_2 )->size( 16 )->color( Color::IDLE )->render(); ?>
								</div>
								<input 
									type="text" 
									class="tutor-input tutor-input-sm tutor-input-content-left" 
									placeholder="<?php echo esc_attr( $this->placeholder ? $this->placeholder : __( 'Search...', 'tutor' ) ); ?>"
									x-model="search"
									@click.stop
								>
							</div>
						</div>
					<?php endif; ?>

					<div class="tutor-overflow-y-auto" style="max-height: 200px;">
						<?php foreach ( $options as $option ) : ?>
							<?php
							$opt_label = $option['label'] ?? '';
							$opt_url   = $option['url'] ?? '#';
							$opt_count = $option['count'] ?? null;
							$is_active = ! empty( $option['active'] );
							$val       = isset( $option['value'] ) ? $option['value'] : '';
							?>
							<?php if ( $this->on_change ) : ?>
								<button
									type="button"
									@click="<?php echo esc_js( $this->on_change ); ?>('<?php echo esc_attr( $val ); ?>'); hide()"
									class="tutor-popover-menu-item <?php echo esc_attr( ! $this->active_value && $is_active ? ' tutor-active' : '' ); ?>"
									<?php if ( $this->active_value ) : ?>
										:class="{ 'tutor-active': (<?php echo esc_attr( $this->active_value ); ?> || '') === '<?php echo esc_attr( $val ); ?>' }"
									<?php endif; ?>
									<?php if ( $this->show_search ) : ?>
										x-show="search === '' || '<?php echo esc_js( $opt_label ); ?>'.toLowerCase().includes(search.toLowerCase())"
									<?php endif; ?>
								>
									<span class="tutor-flex tutor-gap-2 tutor-items-center tutor-w-full">
										<span class="tutor-truncate">
											<?php echo esc_html( $opt_label ); ?>
										</span>
										<?php if ( null !== $opt_count ) : ?>
											<span>
												(<?php echo esc_html( $opt_count ); ?>)
											</span>
										<?php endif; ?>
									</span>
								</button>
							<?php else : ?>
								<a 
									href="<?php echo esc_url( $opt_url ); ?>" 
									class="tutor-popover-menu-item <?php echo $is_active ? 'tutor-active' : ''; ?>"
									<?php if ( $this->show_search ) : ?>
										x-show="search === '' || '<?php echo esc_js( $opt_label ); ?>'.toLowerCase().includes(search.toLowerCase())"
									<?php endif; ?>
								>
									<span class="tutor-flex tutor-gap-2 tutor-items-center tutor-w-full">
										<span class="tutor-truncate">
											<?php echo esc_html( $opt_label ); ?>
										</span>
										<?php if ( null !== $opt_count ) : ?>
											<span>
												(<?php echo esc_html( $opt_count ); ?>)
											</span>
										<?php endif; ?>
									</span>
								</a>
							<?php endif; ?>
						<?php endforeach; ?>

						<?php if ( $this->show_search ) : ?>
							<div 
								x-show="search !== '' && !hasResults" 
								class="tutor-p-5 tutor-text-center tutor-text-secondary tutor-p2"
							>
								<?php esc_html_e( 'No results found', 'tutor' ); ?>
							</div>
						<?php endif; ?>
					</div>
				</div>
			</div>
		</div>
		<?php
		$this->component_string = ob_get_clean();

		return $this->component_string;
	}
}