Constants
19 hours ago
Accordion.php
19 hours ago
Alert.php
19 hours ago
AttachmentCard.php
19 hours ago
Avatar.php
19 hours ago
Badge.php
19 hours ago
BaseComponent.php
19 hours ago
Button.php
19 hours ago
ConfirmationModal.php
19 hours ago
CourseFilter.php
19 hours ago
DateFilter.php
19 hours ago
DropdownFilter.php
19 hours ago
EmptyState.php
19 hours ago
FileUploader.php
19 hours ago
InputField.php
19 hours ago
Modal.php
19 hours ago
Nav.php
19 hours ago
Pagination.php
19 hours ago
Popover.php
19 hours ago
PreviewTrigger.php
19 hours ago
Progress.php
19 hours ago
SearchFilter.php
19 hours ago
Sorting.php
19 hours ago
StarRating.php
19 hours ago
StarRatingInput.php
19 hours ago
StatusSelect.php
19 hours ago
SvgIcon.php
19 hours ago
Table.php
19 hours ago
Tabs.php
19 hours ago
Tooltip.php
19 hours ago
WPEditor.php
19 hours ago
Popover.php
582 lines
| 1 | <?php |
| 2 | /** |
| 3 | * Popover Component Class. |
| 4 | * |
| 5 | * @package Tutor\Components |
| 6 | * @author Themeum |
| 7 | * @link https://themeum.com |
| 8 | * @since 4.0.0 |
| 9 | */ |
| 10 | |
| 11 | namespace Tutor\Components; |
| 12 | |
| 13 | use Tutor\Components\Constants\Positions; |
| 14 | use TUTOR\Icon; |
| 15 | |
| 16 | defined( 'ABSPATH' ) || exit; |
| 17 | |
| 18 | /** |
| 19 | * Class Popover |
| 20 | * |
| 21 | * Responsible for rendering popover component on button |
| 22 | * at different placement positions with footer component and menu items. |
| 23 | * |
| 24 | * Example Usage: Basic Popover |
| 25 | * ``` |
| 26 | * Popover::make() |
| 27 | * ->title( 'Basic' ) |
| 28 | * ->body( '<p>This is a popover component</p>' ) |
| 29 | * ->closeable( true ) |
| 30 | * ->trigger( |
| 31 | * Button::make() |
| 32 | * ->label( 'Show Popover' ) |
| 33 | * ->attr( 'x-ref', 'trigger' ) |
| 34 | * ->attr( '@click', 'toggle()' ) |
| 35 | * ->size( 'medium' ) |
| 36 | * ->variant( 'primary' ) |
| 37 | * ->get() |
| 38 | * ) |
| 39 | * ->render(); |
| 40 | * ``` |
| 41 | * |
| 42 | * @since 4.0.0 |
| 43 | */ |
| 44 | class Popover extends BaseComponent { |
| 45 | |
| 46 | /** |
| 47 | * Transform origin map for popover transitions. |
| 48 | * |
| 49 | * @since 4.0.0 |
| 50 | */ |
| 51 | public const TRANSFORM_ORIGIN_MAP = array( |
| 52 | Positions::TOP => 'center.bottom', |
| 53 | Positions::TOP_START => 'left.bottom', |
| 54 | Positions::TOP_END => 'right.bottom', |
| 55 | Positions::BOTTOM => 'center.top', |
| 56 | Positions::BOTTOM_START => 'left.top', |
| 57 | Positions::BOTTOM_END => 'right.top', |
| 58 | Positions::LEFT => 'right.center', |
| 59 | Positions::LEFT_TOP => 'right.top', |
| 60 | Positions::LEFT_BOTTOM => 'right.bottom', |
| 61 | Positions::RIGHT => 'left.center', |
| 62 | Positions::RIGHT_TOP => 'left.top', |
| 63 | Positions::RIGHT_BOTTOM => 'left.bottom', |
| 64 | ); |
| 65 | |
| 66 | /** |
| 67 | * The popover title. |
| 68 | * |
| 69 | * @var string |
| 70 | */ |
| 71 | protected $popover_title; |
| 72 | |
| 73 | /** |
| 74 | * The popover body. |
| 75 | * |
| 76 | * @var string |
| 77 | */ |
| 78 | protected $popover_body; |
| 79 | |
| 80 | /** |
| 81 | * Callback function for popover body escaping. |
| 82 | * |
| 83 | * @var string |
| 84 | */ |
| 85 | protected $popover_body_esc = 'wp_kses_post'; |
| 86 | |
| 87 | /** |
| 88 | * Allowed HTML tags and attributes. Keys are tag names and values are allowed attributes. |
| 89 | * |
| 90 | * @since 4.0.0 |
| 91 | * |
| 92 | * @var array |
| 93 | */ |
| 94 | protected $allowed_html_tags = array(); |
| 95 | |
| 96 | /** |
| 97 | * The popover placement location (left | right | top | bottom ). |
| 98 | * |
| 99 | * Default 'bottom-start'. |
| 100 | * |
| 101 | * @var string |
| 102 | */ |
| 103 | protected $popover_placement = Positions::BOTTOM_START; |
| 104 | |
| 105 | /** |
| 106 | * Popover button element. |
| 107 | * |
| 108 | * @var string |
| 109 | */ |
| 110 | protected $popover_button; |
| 111 | |
| 112 | /** |
| 113 | * Popover menu items list. |
| 114 | * |
| 115 | * @var array |
| 116 | */ |
| 117 | protected $popover_menu_items; |
| 118 | |
| 119 | /** |
| 120 | * Whether to show or hide popover close button. |
| 121 | * |
| 122 | * Default true. |
| 123 | * |
| 124 | * @var bool |
| 125 | */ |
| 126 | protected $show_close_button = false; |
| 127 | |
| 128 | /** |
| 129 | * Popover footer buttons. |
| 130 | * |
| 131 | * @var array |
| 132 | */ |
| 133 | protected $popover_footer_buttons; |
| 134 | |
| 135 | /** |
| 136 | * Popover footer button alignment class ( left | center ). |
| 137 | * |
| 138 | * Default 'right'. |
| 139 | * |
| 140 | * @var array |
| 141 | */ |
| 142 | protected $popover_footer_alignment; |
| 143 | |
| 144 | /** |
| 145 | * Popover menu item icon svg. |
| 146 | * |
| 147 | * @var string |
| 148 | */ |
| 149 | protected $popover_menu_item_icon; |
| 150 | |
| 151 | /** |
| 152 | * Popover menu item icon alignment. |
| 153 | * |
| 154 | * @var string |
| 155 | */ |
| 156 | protected $popover_menu_item_icon_alignment; |
| 157 | |
| 158 | /** |
| 159 | * Popover menu item custom class. |
| 160 | * |
| 161 | * @var string |
| 162 | */ |
| 163 | protected $popover_menu_item_class; |
| 164 | |
| 165 | /** |
| 166 | * Attributes of Popover menu items. |
| 167 | * |
| 168 | * @var array |
| 169 | */ |
| 170 | protected $attributes; |
| 171 | |
| 172 | /** |
| 173 | * Prevent close from outside |
| 174 | * |
| 175 | * @var bool |
| 176 | */ |
| 177 | protected $popover_close_outside = true; |
| 178 | |
| 179 | /** |
| 180 | * Menu min width. |
| 181 | * |
| 182 | * @var string |
| 183 | */ |
| 184 | protected $menu_min_width; |
| 185 | |
| 186 | /** |
| 187 | * Set Popover title |
| 188 | * |
| 189 | * @since 4.0.0 |
| 190 | * |
| 191 | * @param string $popover_title the popover title. |
| 192 | * |
| 193 | * @return self |
| 194 | */ |
| 195 | public function title( string $popover_title ): self { |
| 196 | $this->popover_title = $popover_title; |
| 197 | return $this; |
| 198 | } |
| 199 | |
| 200 | /** |
| 201 | * Set Popover body html. |
| 202 | * |
| 203 | * @since 4.0.0 |
| 204 | * |
| 205 | * @param string $popover_body the popover body html. |
| 206 | * @param array $allowed_html_tags html tags to allow. |
| 207 | * |
| 208 | * @return self |
| 209 | */ |
| 210 | public function body( string $popover_body, array $allowed_html_tags = array() ): self { |
| 211 | $this->popover_body = $popover_body; |
| 212 | $this->allowed_html_tags = $allowed_html_tags; |
| 213 | return $this; |
| 214 | } |
| 215 | |
| 216 | /** |
| 217 | * Set popover placement position. |
| 218 | * |
| 219 | * @since 4.0.0 |
| 220 | * |
| 221 | * @param string $popover_placement the placement position. |
| 222 | * |
| 223 | * @return self |
| 224 | */ |
| 225 | public function placement( string $popover_placement = 'bottom-start' ): self { |
| 226 | $placement_positions = array( |
| 227 | Positions::TOP, |
| 228 | Positions::TOP_START, |
| 229 | Positions::TOP_END, |
| 230 | Positions::LEFT, |
| 231 | Positions::LEFT_TOP, |
| 232 | Positions::LEFT_BOTTOM, |
| 233 | Positions::RIGHT, |
| 234 | Positions::RIGHT_TOP, |
| 235 | Positions::RIGHT_BOTTOM, |
| 236 | Positions::BOTTOM, |
| 237 | Positions::BOTTOM_START, |
| 238 | Positions::BOTTOM_END, |
| 239 | ); |
| 240 | if ( ! in_array( $popover_placement, $placement_positions, true ) ) { |
| 241 | $this->popover_placement = Positions::BOTTOM_START; |
| 242 | return $this; |
| 243 | } |
| 244 | |
| 245 | $this->popover_placement = $popover_placement; |
| 246 | return $this; |
| 247 | } |
| 248 | |
| 249 | /** |
| 250 | * Popover button component. |
| 251 | * |
| 252 | * @since 4.0.0 |
| 253 | * |
| 254 | * @param string $popover_button the popover button element html. |
| 255 | * |
| 256 | * @return self |
| 257 | */ |
| 258 | public function trigger( string $popover_button ): self { |
| 259 | $this->popover_button = $popover_button; |
| 260 | return $this; |
| 261 | } |
| 262 | |
| 263 | /** |
| 264 | * Whether to hide close button on popover. |
| 265 | * |
| 266 | * @since 4.0.0 |
| 267 | * |
| 268 | * @param boolean $show_close_button determines whether to hide close button. |
| 269 | * |
| 270 | * @return self |
| 271 | */ |
| 272 | public function closeable( bool $show_close_button = false ): self { |
| 273 | $this->show_close_button = $show_close_button; |
| 274 | return $this; |
| 275 | } |
| 276 | |
| 277 | /** |
| 278 | * Set the popover footer alignment. |
| 279 | * |
| 280 | * @since 4.0.0 |
| 281 | * |
| 282 | * @param string $popover_footer_alignment the popover footer alignment class. |
| 283 | * |
| 284 | * @return self |
| 285 | */ |
| 286 | public function footer_alignment( string $popover_footer_alignment ): self { |
| 287 | $this->popover_footer_alignment = $popover_footer_alignment; |
| 288 | return $this; |
| 289 | } |
| 290 | |
| 291 | /** |
| 292 | * Set popover footer buttons. |
| 293 | * |
| 294 | * @since 4.0.0 |
| 295 | * |
| 296 | * @param array $popover_footer_buttons the footer button list. |
| 297 | * |
| 298 | * @return self |
| 299 | */ |
| 300 | public function footer( array $popover_footer_buttons = array() ): self { |
| 301 | $this->popover_footer_buttons = $popover_footer_buttons; |
| 302 | return $this; |
| 303 | } |
| 304 | |
| 305 | /** |
| 306 | * Menu items for creating popover menu. |
| 307 | * |
| 308 | * @since 4.0.0 |
| 309 | * |
| 310 | * @param array $args { |
| 311 | * Array of arguments to be provided for the menu items. |
| 312 | * |
| 313 | * @type string $tag the tag of the menu item. |
| 314 | * @type string $class the class for the menu items. |
| 315 | * @type string $icon the icon svg. |
| 316 | * @type string $icon_alignment the alignment of menu item icon (left | right). |
| 317 | * @type string $content the popover menu item content. |
| 318 | * @type array $attr the popover menu item attributes. |
| 319 | * } |
| 320 | * |
| 321 | * @return self |
| 322 | */ |
| 323 | public function menu_item( array $args ): self { |
| 324 | $menu_item_tag = isset( $args['tag'] ) ? $this->esc( $args['tag'], $this->popover_body_esc ) : ''; |
| 325 | $this->popover_menu_item_class = $args['class'] ?? ''; |
| 326 | $this->popover_menu_item_icon = $args['icon'] ?? ''; |
| 327 | $this->popover_menu_item_icon_alignment = isset( $args['icon_alignment'] ) && in_array( $args['icon_alignment'], array( Positions::LEFT, Positions::RIGHT ), true ) ? $args['icon_alignment'] : Positions::LEFT; |
| 328 | |
| 329 | $this->popover_menu_items[] = array( |
| 330 | 'tag' => $menu_item_tag, |
| 331 | 'content' => $args['content'] ?? '', |
| 332 | 'class' => $this->popover_menu_item_class, |
| 333 | 'icon' => $this->popover_menu_item_icon, |
| 334 | 'icon_alignment' => $this->popover_menu_item_icon_alignment, |
| 335 | 'attr' => $args['attr'] ?? array(), |
| 336 | ); |
| 337 | |
| 338 | return $this; |
| 339 | } |
| 340 | |
| 341 | /** |
| 342 | * Set menu min width. |
| 343 | * |
| 344 | * @since 4.0.0 |
| 345 | * |
| 346 | * @param string $width the min width value. |
| 347 | * |
| 348 | * @return self |
| 349 | */ |
| 350 | public function menu_min_width( string $width ): self { |
| 351 | $this->menu_min_width = $width; |
| 352 | return $this; |
| 353 | } |
| 354 | |
| 355 | /** |
| 356 | * Render popover header element. |
| 357 | * |
| 358 | * @since 4.0.0 |
| 359 | * |
| 360 | * @return string |
| 361 | */ |
| 362 | protected function render_header(): string { |
| 363 | if ( empty( $this->popover_title ) ) { |
| 364 | return ''; |
| 365 | } |
| 366 | |
| 367 | $title = sprintf( '<h3 class="tutor-popover-title">%s</h3>', esc_attr( $this->popover_title ) ); |
| 368 | |
| 369 | $close_button = sprintf( |
| 370 | '<button @click="hide()" class="tutor-popover-close" aria-label="%s"> |
| 371 | %s |
| 372 | </button>', |
| 373 | esc_attr__( 'Close', 'tutor' ), |
| 374 | SvgIcon::make()->name( Icon::CROSS )->size( 14 )->get() |
| 375 | ); |
| 376 | |
| 377 | if ( $this->show_close_button ) { |
| 378 | return sprintf( |
| 379 | '<div class="tutor-popover-header"> |
| 380 | %s |
| 381 | %s |
| 382 | </div> |
| 383 | ', |
| 384 | $title, |
| 385 | $close_button |
| 386 | ); |
| 387 | } |
| 388 | |
| 389 | return sprintf( |
| 390 | '<div class="tutor-popover-header"> |
| 391 | %s |
| 392 | </div> |
| 393 | ', |
| 394 | $title, |
| 395 | ); |
| 396 | } |
| 397 | |
| 398 | /** |
| 399 | * Render popover body component. |
| 400 | * |
| 401 | * @since 4.0.0 |
| 402 | * |
| 403 | * @return string |
| 404 | */ |
| 405 | protected function render_body(): string { |
| 406 | if ( empty( $this->popover_body ) ) { |
| 407 | return ''; |
| 408 | } |
| 409 | |
| 410 | $body = wp_kses( $this->popover_body, $this->get_allowed_html_tags( $this->allowed_html_tags ) ); |
| 411 | |
| 412 | return sprintf( |
| 413 | '<div class="tutor-popover-body"> |
| 414 | %s |
| 415 | </div>', |
| 416 | $body |
| 417 | ); |
| 418 | } |
| 419 | |
| 420 | /** |
| 421 | * Render footer component for popover. |
| 422 | * |
| 423 | * @since 4.0.0 |
| 424 | * |
| 425 | * @return string |
| 426 | */ |
| 427 | protected function render_footer(): string { |
| 428 | $footer_buttons = ''; |
| 429 | $footer_alignment = in_array( $this->popover_footer_alignment, array( Positions::LEFT, Positions::CENTER ), true ) ? $this->popover_footer_alignment : ''; |
| 430 | |
| 431 | if ( ! tutor_utils()->count( $this->popover_footer_buttons ) ) { |
| 432 | return $footer_buttons; |
| 433 | } |
| 434 | |
| 435 | $alignment = Positions::LEFT === $footer_alignment ? 'tutor-justify-start' : 'tutor-justify-center'; |
| 436 | |
| 437 | foreach ( $this->popover_footer_buttons as $button ) { |
| 438 | $footer_buttons .= $button; |
| 439 | } |
| 440 | |
| 441 | if ( ! empty( $footer_alignment ) ) { |
| 442 | return sprintf( |
| 443 | '<div class="tutor-popover-footer %s"> |
| 444 | %s |
| 445 | </div>', |
| 446 | $alignment, |
| 447 | $footer_buttons |
| 448 | ); |
| 449 | } |
| 450 | |
| 451 | return sprintf( |
| 452 | '<div class="tutor-popover-footer"> |
| 453 | %s |
| 454 | </div>', |
| 455 | $footer_buttons |
| 456 | ); |
| 457 | } |
| 458 | |
| 459 | /** |
| 460 | * Handle closing popover on outside click. |
| 461 | * |
| 462 | * @since 4.0.0 |
| 463 | * |
| 464 | * @param boolean $is_closeable whether allow to close from outside click. |
| 465 | * |
| 466 | * @return self |
| 467 | */ |
| 468 | public function dismissible( bool $is_closeable ): self { |
| 469 | $this->popover_close_outside = $is_closeable; |
| 470 | return $this; |
| 471 | } |
| 472 | |
| 473 | /** |
| 474 | * Render Menu for popover. |
| 475 | * |
| 476 | * @since 4.0.0 |
| 477 | * |
| 478 | * @return string |
| 479 | */ |
| 480 | protected function render_menu(): string { |
| 481 | $menu_items = ''; |
| 482 | if ( ! tutor_utils()->count( $this->popover_menu_items ) ) { |
| 483 | return $menu_items; |
| 484 | } |
| 485 | |
| 486 | foreach ( $this->popover_menu_items as $item ) { |
| 487 | $tag = $item['tag'] ?? 'div'; |
| 488 | $content = isset( $item['content'] ) ? $this->esc( $item['content'], $this->popover_body_esc ) : ''; |
| 489 | $class = isset( $item['class'] ) ? esc_attr( $item['class'] ) : ''; |
| 490 | $icon = $item['icon'] ?? ''; |
| 491 | $icon_alignment = $item['icon_alignment'] ?? Positions::LEFT; |
| 492 | |
| 493 | $this->attributes = tutor_utils()->count( $item['attr'] ) ? $item['attr'] : array(); |
| 494 | |
| 495 | $menu_item_attr = $this->get_attributes_string(); |
| 496 | |
| 497 | if ( empty( $icon ) ) { |
| 498 | $menu_items .= sprintf( |
| 499 | '<%1$s class="tutor-popover-menu-item %2$s" %4$s>%3$s</%1$s>', |
| 500 | $tag, |
| 501 | $class, |
| 502 | $content, |
| 503 | $menu_item_attr |
| 504 | ); |
| 505 | } elseif ( Positions::RIGHT === $icon_alignment ) { |
| 506 | $menu_items .= sprintf( |
| 507 | '<%1$s class="tutor-popover-menu-item %2$s" %5$s>%3$s%4$s</%1$s>', |
| 508 | $tag, |
| 509 | $class, |
| 510 | $content, |
| 511 | $icon, |
| 512 | $menu_item_attr |
| 513 | ); |
| 514 | } else { |
| 515 | $menu_items .= sprintf( |
| 516 | '<%1$s class="tutor-popover-menu-item %2$s" %5$s>%4$s%3$s</%1$s>', |
| 517 | $tag, |
| 518 | $class, |
| 519 | $content, |
| 520 | $icon, |
| 521 | $menu_item_attr |
| 522 | ); |
| 523 | } |
| 524 | } |
| 525 | |
| 526 | $style = $this->menu_min_width ? " style=\"min-width: {$this->menu_min_width}\"" : ''; |
| 527 | return sprintf( '<div class="tutor-popover-menu"%s>%s</div>', $style, $menu_items ); |
| 528 | } |
| 529 | |
| 530 | /** |
| 531 | * Get the final Popover HTML Component. |
| 532 | * |
| 533 | * @since 4.0.0 |
| 534 | * |
| 535 | * @return string HTML content. |
| 536 | */ |
| 537 | public function get(): string { |
| 538 | |
| 539 | $header = $this->render_header(); |
| 540 | $body = $this->render_body(); |
| 541 | $placement_position = $this->popover_placement; |
| 542 | $button = $this->popover_button ?? ''; |
| 543 | $footer = $this->render_footer(); |
| 544 | $menu = $this->render_menu(); |
| 545 | |
| 546 | $placement_class = 'tutor-popover-' . explode( '-', $placement_position )[0]; |
| 547 | $class = 'tutor-popover ' . $placement_class; |
| 548 | |
| 549 | $closeable_attr = $this->popover_close_outside ? '@click.outside="handleClickOutside()"' : ''; |
| 550 | |
| 551 | $origin = self::TRANSFORM_ORIGIN_MAP[ $placement_position ] ?? 'center.top'; |
| 552 | |
| 553 | return sprintf( |
| 554 | '<div x-data="tutorPopover({ placement: \'%s\' })"> |
| 555 | %s |
| 556 | <div |
| 557 | x-ref="content" |
| 558 | x-show="open" |
| 559 | x-cloak |
| 560 | x-transition.%s |
| 561 | class="%s" |
| 562 | %s |
| 563 | > |
| 564 | %s |
| 565 | %s |
| 566 | %s |
| 567 | %s |
| 568 | </div> |
| 569 | </div>', |
| 570 | $placement_position, |
| 571 | $button, |
| 572 | $origin, |
| 573 | $class, |
| 574 | $closeable_attr, |
| 575 | $header, |
| 576 | $body, |
| 577 | $footer, |
| 578 | $menu |
| 579 | ); |
| 580 | } |
| 581 | } |
| 582 |