Constants
21 hours ago
Accordion.php
21 hours ago
Alert.php
21 hours ago
AttachmentCard.php
21 hours ago
Avatar.php
21 hours ago
Badge.php
21 hours ago
BaseComponent.php
21 hours ago
Button.php
21 hours ago
ConfirmationModal.php
21 hours ago
CourseFilter.php
21 hours ago
DateFilter.php
21 hours ago
DropdownFilter.php
21 hours ago
EmptyState.php
21 hours ago
FileUploader.php
21 hours ago
InputField.php
21 hours ago
Modal.php
21 hours ago
Nav.php
21 hours ago
Pagination.php
21 hours ago
Popover.php
21 hours ago
PreviewTrigger.php
21 hours ago
Progress.php
21 hours ago
SearchFilter.php
21 hours ago
Sorting.php
21 hours ago
StarRating.php
21 hours ago
StarRatingInput.php
21 hours ago
StatusSelect.php
21 hours ago
SvgIcon.php
21 hours ago
Table.php
21 hours ago
Tabs.php
21 hours ago
Tooltip.php
21 hours ago
WPEditor.php
21 hours ago
DateFilter.php
423 lines
| 1 | <?php |
| 2 | /** |
| 3 | * Date Filter Component. |
| 4 | * |
| 5 | * @package Tutor\Components |
| 6 | * @author Themeum |
| 7 | * @link https://themeum.com |
| 8 | * @since 4.0.0 |
| 9 | */ |
| 10 | |
| 11 | namespace Tutor\Components; |
| 12 | |
| 13 | use TUTOR\Icon; |
| 14 | use TUTOR\Input; |
| 15 | use Tutor\Components\Constants\Size; |
| 16 | use Tutor\Components\Popover; |
| 17 | |
| 18 | defined( 'ABSPATH' ) || exit; |
| 19 | |
| 20 | /** |
| 21 | * DateFilter Component Class. |
| 22 | * |
| 23 | * Example usage: |
| 24 | * |
| 25 | * ```php |
| 26 | * DateFilter::make() |
| 27 | * ->type( DateFilter::TYPE_RANGE ) |
| 28 | * ->placement( DateFilter::PLACEMENT_BOTTOM_START ) |
| 29 | * ->render(); |
| 30 | * ``` |
| 31 | * |
| 32 | * ```php |
| 33 | * DateFilter::make() |
| 34 | * ->type( DateFilter::TYPE_SINGLE ) |
| 35 | * ->placement( DateFilter::PLACEMENT_BOTTOM_END ) |
| 36 | * ->render(); |
| 37 | * ``` |
| 38 | * |
| 39 | * @since 4.0.0 |
| 40 | */ |
| 41 | class DateFilter extends BaseComponent { |
| 42 | |
| 43 | /** |
| 44 | * Date Filter Type Single |
| 45 | */ |
| 46 | const TYPE_SINGLE = 'single'; |
| 47 | |
| 48 | /** |
| 49 | * Date Filter Type Range |
| 50 | */ |
| 51 | const TYPE_RANGE = 'range'; |
| 52 | |
| 53 | /** |
| 54 | * Placement Bottom Start |
| 55 | */ |
| 56 | const PLACEMENT_BOTTOM_START = 'bottom-start'; |
| 57 | |
| 58 | /** |
| 59 | * Placement Bottom End |
| 60 | */ |
| 61 | const PLACEMENT_BOTTOM_END = 'bottom-end'; |
| 62 | |
| 63 | /** |
| 64 | * Component Type |
| 65 | * |
| 66 | * @var string |
| 67 | */ |
| 68 | protected $type = self::TYPE_SINGLE; |
| 69 | |
| 70 | /** |
| 71 | * Button Label |
| 72 | * |
| 73 | * @var string |
| 74 | */ |
| 75 | protected $label = ''; |
| 76 | |
| 77 | /** |
| 78 | * Popover placement |
| 79 | * |
| 80 | * @var string |
| 81 | */ |
| 82 | protected $placement = self::PLACEMENT_BOTTOM_START; |
| 83 | |
| 84 | /** |
| 85 | * Button size. |
| 86 | * |
| 87 | * @var string |
| 88 | */ |
| 89 | protected $size = Size::SMALL; |
| 90 | |
| 91 | /** |
| 92 | * Icon size. |
| 93 | * |
| 94 | * @var integer |
| 95 | */ |
| 96 | protected $icon_size = Size::SIZE_16; |
| 97 | |
| 98 | /** |
| 99 | * Whether to display the label text. |
| 100 | * |
| 101 | * @since 4.0.0 |
| 102 | * |
| 103 | * @var bool |
| 104 | */ |
| 105 | protected $show_label = true; |
| 106 | |
| 107 | /** |
| 108 | * Whether to hide the initial label when no date range is selected. |
| 109 | * |
| 110 | * @since 4.0.0 |
| 111 | * |
| 112 | * @var bool |
| 113 | */ |
| 114 | protected $hide_initial_label = false; |
| 115 | |
| 116 | /** |
| 117 | * Query params to clear when a date filter is applied or cleared (e.g. 'current_page'). |
| 118 | * |
| 119 | * @since 4.0.0 |
| 120 | * |
| 121 | * @var array |
| 122 | */ |
| 123 | protected $clear_params = array(); |
| 124 | |
| 125 | /** |
| 126 | * Set filter type. |
| 127 | * |
| 128 | * @param string $type Filter type (single|range). |
| 129 | * |
| 130 | * @return self |
| 131 | */ |
| 132 | public function type( string $type ): self { |
| 133 | $this->type = $type; |
| 134 | return $this; |
| 135 | } |
| 136 | |
| 137 | /** |
| 138 | * Set Button Size. |
| 139 | * |
| 140 | * @since 4.0.0 |
| 141 | * |
| 142 | * @param string $size the size type (x-small|small|medium|large). |
| 143 | * |
| 144 | * @return self |
| 145 | */ |
| 146 | public function trigger_size( string $size ): self { |
| 147 | $allowed_sizes = array( Size::X_SMALL, Size::SMALL, Size::MEDIUM, Size::LARGE ); |
| 148 | if ( in_array( $size, $allowed_sizes, true ) ) { |
| 149 | $this->size = $size; |
| 150 | } |
| 151 | return $this; |
| 152 | } |
| 153 | |
| 154 | /** |
| 155 | * Set the button icon size. |
| 156 | * |
| 157 | * @since 4.0.0 |
| 158 | * |
| 159 | * @param integer $size the icon size. |
| 160 | * |
| 161 | * @return self |
| 162 | */ |
| 163 | public function icon_size( int $size ): self { |
| 164 | $this->icon_size = $size; |
| 165 | return $this; |
| 166 | } |
| 167 | |
| 168 | /** |
| 169 | * Set button label. |
| 170 | * |
| 171 | * @param string $label Button label. |
| 172 | * |
| 173 | * @return self |
| 174 | */ |
| 175 | public function label( string $label ): self { |
| 176 | $this->label = $label; |
| 177 | return $this; |
| 178 | } |
| 179 | |
| 180 | /** |
| 181 | * Set popover placement. |
| 182 | * |
| 183 | * @param string $placement Popover placement. |
| 184 | * |
| 185 | * @return self |
| 186 | */ |
| 187 | public function placement( string $placement ): self { |
| 188 | $this->placement = $placement; |
| 189 | return $this; |
| 190 | } |
| 191 | |
| 192 | /** |
| 193 | * Enable or disable the display of the label text. |
| 194 | * |
| 195 | * @since 4.0.0 |
| 196 | * |
| 197 | * @param bool $show_label True to show the label, false to hide it. |
| 198 | * |
| 199 | * @return $this |
| 200 | */ |
| 201 | public function show_label( bool $show_label ) { |
| 202 | $this->show_label = $show_label; |
| 203 | return $this; |
| 204 | } |
| 205 | |
| 206 | /** |
| 207 | * Hide or show the initial label text before a date range is selected. |
| 208 | * |
| 209 | * @since 4.0.0 |
| 210 | * |
| 211 | * @return self |
| 212 | */ |
| 213 | public function hide_initial_label(): self { |
| 214 | $this->hide_initial_label = true; |
| 215 | return $this; |
| 216 | } |
| 217 | |
| 218 | /** |
| 219 | * Set query params to clear when a date filter is applied or cleared. |
| 220 | * |
| 221 | * Useful for resetting pagination (e.g. 'current_page') or other |
| 222 | * context-specific params whenever the date selection changes. |
| 223 | * |
| 224 | * @since 4.0.0 |
| 225 | * |
| 226 | * @param array $params List of query param keys to remove on apply/clear. |
| 227 | * |
| 228 | * @return self |
| 229 | */ |
| 230 | public function clear_params( array $params ): self { |
| 231 | $this->clear_params = $params; |
| 232 | return $this; |
| 233 | } |
| 234 | |
| 235 | /** |
| 236 | * Render the component. |
| 237 | * |
| 238 | * @return string |
| 239 | */ |
| 240 | public function get(): string { |
| 241 | $is_range = self::TYPE_RANGE === $this->type; |
| 242 | |
| 243 | if ( empty( $this->label ) ) { |
| 244 | $this->label = $this->calculate_label(); |
| 245 | } |
| 246 | |
| 247 | // Default settings based on type. |
| 248 | $calendar_options = array( |
| 249 | 'type' => 'default', |
| 250 | 'clearParams' => $this->clear_params, |
| 251 | ); |
| 252 | |
| 253 | $button_classes = 'tutor-btn tutor-btn-outline'; |
| 254 | |
| 255 | if ( $this->size ) { |
| 256 | $button_classes .= ' tutor-btn-' . $this->size; |
| 257 | } |
| 258 | |
| 259 | $popover_classes = 'tutor-popover'; |
| 260 | $icon = Icon::CALENDAR_2; |
| 261 | |
| 262 | if ( $is_range ) { |
| 263 | $calendar_options = array( |
| 264 | 'type' => 'multiple', |
| 265 | 'selectionDatesMode' => 'multiple-ranged', |
| 266 | 'clearParams' => $this->clear_params, |
| 267 | ); |
| 268 | $popover_classes .= ' tutor-range-calendar-popover'; |
| 269 | } |
| 270 | |
| 271 | if ( empty( $this->label ) ) { |
| 272 | $button_classes .= ' tutor-btn-icon'; |
| 273 | } else { |
| 274 | $button_classes .= ' tutor-gap-2'; |
| 275 | } |
| 276 | |
| 277 | $options_json = wp_json_encode( $calendar_options ); |
| 278 | |
| 279 | $origin = Popover::TRANSFORM_ORIGIN_MAP[ $this->placement ] ?? 'center.top'; |
| 280 | |
| 281 | ob_start(); |
| 282 | ?> |
| 283 | <div x-data="tutorPopover({ placement: '<?php echo esc_attr( $this->placement ); ?>' })" <?php echo $this->get_attributes_string(); //phpcs:ignore -- Sanitization is performed inside get_attributes_string. ?>> |
| 284 | |
| 285 | <button |
| 286 | type="button" |
| 287 | x-ref="trigger" |
| 288 | @click="toggle()" |
| 289 | class="<?php echo esc_attr( $button_classes ); ?>" |
| 290 | <?php if ( empty( $this->label ) ) : ?> |
| 291 | aria-label="<?php echo $is_range ? esc_attr__( 'Filter by date range', 'tutor' ) : esc_attr__( 'Filter by date', 'tutor' ); ?>" |
| 292 | <?php endif; ?> |
| 293 | > |
| 294 | <?php SvgIcon::make()->name( $icon )->size( $this->icon_size )->render(); ?> |
| 295 | <?php if ( ! empty( $this->label ) ) : ?> |
| 296 | <span><?php echo esc_html( $this->label ); ?></span> |
| 297 | <?php endif; ?> |
| 298 | |
| 299 | <?php if ( $this->has_selection() ) : ?> |
| 300 | <span @click.stop="$dispatch('tutor-calendar:clear')" class="tutor-cursor-pointer tutor-icon-secondary tutor-flex tutor-align-center"> |
| 301 | <?php SvgIcon::make()->name( Icon::CROSS_2 )->render(); ?> |
| 302 | </span> |
| 303 | <?php endif; ?> |
| 304 | </button> |
| 305 | |
| 306 | <div |
| 307 | x-ref="content" |
| 308 | x-show="open" |
| 309 | x-cloak |
| 310 | x-transition.origin.<?php echo esc_attr( $origin ); ?> |
| 311 | @click.outside="handleClickOutside()" |
| 312 | class="<?php echo esc_attr( $popover_classes ); ?>" |
| 313 | > |
| 314 | <div x-data="tutorCalendar({ options: <?php echo esc_attr( $options_json ); ?>, hidePopover: () => hide() })"></div> |
| 315 | </div> |
| 316 | </div> |
| 317 | <?php |
| 318 | return ob_get_clean(); |
| 319 | } |
| 320 | |
| 321 | /** |
| 322 | * Check if filter has active selection. |
| 323 | * |
| 324 | * @return bool |
| 325 | */ |
| 326 | protected function has_selection(): bool { |
| 327 | if ( self::TYPE_SINGLE === $this->type ) { |
| 328 | return ! empty( Input::get( 'date' ) ); |
| 329 | } |
| 330 | return ! empty( Input::get( 'start_date' ) ) && ! empty( Input::get( 'end_date' ) ); |
| 331 | } |
| 332 | |
| 333 | /** |
| 334 | * Calculate dynamic label based on URL parameters. |
| 335 | * |
| 336 | * @return string |
| 337 | */ |
| 338 | protected function calculate_label(): string { |
| 339 | if ( self::TYPE_SINGLE === $this->type ) { |
| 340 | return Input::get( 'date', '' ); |
| 341 | } |
| 342 | |
| 343 | if ( ! $this->show_label ) { |
| 344 | return ''; |
| 345 | } |
| 346 | |
| 347 | $start_date = Input::get( 'start_date' ); |
| 348 | $end_date = Input::get( ( 'end_date' ) ); |
| 349 | |
| 350 | if ( ! $start_date || ! $end_date ) { |
| 351 | return $this->hide_initial_label ? '' : __( 'All Time', 'tutor' ); |
| 352 | } |
| 353 | |
| 354 | $presets = array( |
| 355 | 'today' => __( 'Today', 'tutor' ), |
| 356 | 'yesterday' => __( 'Yesterday', 'tutor' ), |
| 357 | 'last-7' => __( 'Last 7 Days', 'tutor' ), |
| 358 | 'last-14' => __( 'Last 14 Days', 'tutor' ), |
| 359 | 'last-30' => __( 'Last 30 Days', 'tutor' ), |
| 360 | 'this-month' => __( 'This Month', 'tutor' ), |
| 361 | 'last-month' => __( 'Last Month', 'tutor' ), |
| 362 | 'last-year' => __( 'Last Year', 'tutor' ), |
| 363 | ); |
| 364 | |
| 365 | $current_time = time(); |
| 366 | |
| 367 | foreach ( $presets as $key => $label ) { |
| 368 | $range = $this->get_preset_dates( $key, $current_time ); |
| 369 | if ( $range && $range[0] === $start_date && $range[1] === $end_date ) { |
| 370 | return $label; |
| 371 | } |
| 372 | } |
| 373 | |
| 374 | $start_format = date_i18n( 'M j', strtotime( $start_date ) ); |
| 375 | $end_format = date_i18n( 'M j', strtotime( $end_date ) ); |
| 376 | |
| 377 | return sprintf( '%s - %s', $start_format, $end_format ); |
| 378 | } |
| 379 | |
| 380 | /** |
| 381 | * Get preset dates. |
| 382 | * |
| 383 | * @param string $preset Preset key. |
| 384 | * @param int $current_time Current timestamp. |
| 385 | * |
| 386 | * @return array|null |
| 387 | */ |
| 388 | protected function get_preset_dates( $preset, $current_time ) { |
| 389 | $today = gmdate( 'Y-m-d', $current_time ); |
| 390 | |
| 391 | switch ( $preset ) { |
| 392 | case 'today': |
| 393 | return array( $today, $today ); |
| 394 | case 'yesterday': |
| 395 | $d = gmdate( 'Y-m-d', strtotime( '-1 day', $current_time ) ); |
| 396 | return array( $d, $d ); |
| 397 | case 'last-7': |
| 398 | $start = gmdate( 'Y-m-d', strtotime( '-6 days', $current_time ) ); |
| 399 | return array( $start, $today ); |
| 400 | case 'last-14': |
| 401 | $start = gmdate( 'Y-m-d', strtotime( '-13 days', $current_time ) ); |
| 402 | return array( $start, $today ); |
| 403 | case 'last-30': |
| 404 | $start = gmdate( 'Y-m-d', strtotime( '-29 days', $current_time ) ); |
| 405 | return array( $start, $today ); |
| 406 | case 'this-month': |
| 407 | $start = gmdate( 'Y-m-01', $current_time ); |
| 408 | $end = gmdate( 'Y-m-t', $current_time ); |
| 409 | return array( $start, $end ); |
| 410 | case 'last-month': |
| 411 | $first_day_last_month = strtotime( 'first day of previous month', $current_time ); |
| 412 | $start = gmdate( 'Y-m-d', $first_day_last_month ); |
| 413 | $end = gmdate( 'Y-m-t', $first_day_last_month ); |
| 414 | return array( $start, $end ); |
| 415 | case 'last-year': |
| 416 | $start = gmdate( 'Y-01-01', strtotime( '-1 year', $current_time ) ); |
| 417 | $end = gmdate( 'Y-12-31', strtotime( '-1 year', $current_time ) ); |
| 418 | return array( $start, $end ); |
| 419 | } |
| 420 | return null; |
| 421 | } |
| 422 | } |
| 423 |