Constants
22 hours ago
Accordion.php
22 hours ago
Alert.php
22 hours ago
AttachmentCard.php
22 hours ago
Avatar.php
22 hours ago
Badge.php
22 hours ago
BaseComponent.php
22 hours ago
Button.php
22 hours ago
ConfirmationModal.php
22 hours ago
CourseFilter.php
22 hours ago
DateFilter.php
22 hours ago
DropdownFilter.php
22 hours ago
EmptyState.php
22 hours ago
FileUploader.php
22 hours ago
InputField.php
22 hours ago
Modal.php
22 hours ago
Nav.php
22 hours ago
Pagination.php
22 hours ago
Popover.php
22 hours ago
PreviewTrigger.php
22 hours ago
Progress.php
22 hours ago
SearchFilter.php
22 hours ago
Sorting.php
22 hours ago
StarRating.php
22 hours ago
StarRatingInput.php
22 hours ago
StatusSelect.php
22 hours ago
SvgIcon.php
22 hours ago
Table.php
22 hours ago
Tabs.php
22 hours ago
Tooltip.php
22 hours ago
WPEditor.php
22 hours ago
Modal.php
527 lines
| 1 | <?php |
| 2 | /** |
| 3 | * Tutor Component: Modal |
| 4 | * |
| 5 | * Provides a fluent builder for rendering modals with Alpine.js integration. |
| 6 | * |
| 7 | * @package Tutor\Components |
| 8 | * @author Themeum |
| 9 | * @link https://themeum.com |
| 10 | * @since 4.0.0 |
| 11 | */ |
| 12 | |
| 13 | namespace Tutor\Components; |
| 14 | |
| 15 | defined( 'ABSPATH' ) || exit; |
| 16 | |
| 17 | /** |
| 18 | * Modal Component Class. |
| 19 | * |
| 20 | * ``` |
| 21 | * // Example usage: |
| 22 | * |
| 23 | * Modal::make() |
| 24 | * ->id( 'confirm-modal' ) |
| 25 | * ->title( 'Confirm Submission' ) |
| 26 | * ->subtitle( 'Are you sure you want to submit?' ) |
| 27 | * ->body( 'This action cannot be undone.' ) |
| 28 | * ->footer_buttons( '<button class="tutor-btn">Cancel</button>' ) |
| 29 | * ->footer_alignment( 'right' ) |
| 30 | * ->render(); |
| 31 | * |
| 32 | * // With template path |
| 33 | * Modal::make() |
| 34 | * ->id( 'course-modal' ) |
| 35 | * ->title( 'Course Details' ) |
| 36 | * ->template( 'path/to/template.php' ) |
| 37 | * ->render(); |
| 38 | * |
| 39 | * // Headless modal (no close button) |
| 40 | * Modal::make() |
| 41 | * ->id( 'headless-modal' ) |
| 42 | * ->closeable( false ) |
| 43 | * ->body( '<h3>Success!</h3>' ) |
| 44 | * ->render(); |
| 45 | * |
| 46 | * // Open by default (state is 'closed' by default) |
| 47 | * Modal::make() |
| 48 | * ->id( 'welcome-modal' ) |
| 49 | * ->title( 'Welcome' ) |
| 50 | * ->body( 'Please confirm to continue.' ) |
| 51 | * ->state( 'open' ) |
| 52 | * ->render(); |
| 53 | * ``` |
| 54 | * |
| 55 | * @since 4.0.0 |
| 56 | */ |
| 57 | class Modal extends BaseComponent { |
| 58 | |
| 59 | /** |
| 60 | * Modal unique ID. |
| 61 | * |
| 62 | * @since 4.0.0 |
| 63 | * |
| 64 | * @var string |
| 65 | */ |
| 66 | protected $id = ''; |
| 67 | |
| 68 | /** |
| 69 | * Modal title. |
| 70 | * |
| 71 | * @since 4.0.0 |
| 72 | * |
| 73 | * @var string |
| 74 | */ |
| 75 | protected $title = ''; |
| 76 | |
| 77 | /** |
| 78 | * Modal title icon. |
| 79 | * |
| 80 | * @since 4.0.0 |
| 81 | * |
| 82 | * @var string |
| 83 | */ |
| 84 | protected $title_icon = ''; |
| 85 | |
| 86 | /** |
| 87 | * Modal title esc func. |
| 88 | * |
| 89 | * @since 4.0.0 |
| 90 | * |
| 91 | * @var string |
| 92 | */ |
| 93 | protected $title_esc_cb = 'esc_html'; |
| 94 | |
| 95 | /** |
| 96 | * Modal subtitle. |
| 97 | * |
| 98 | * @since 4.0.0 |
| 99 | * |
| 100 | * @var string |
| 101 | */ |
| 102 | protected $subtitle = ''; |
| 103 | |
| 104 | /** |
| 105 | * Modal subtitle esc func. |
| 106 | * |
| 107 | * @since 4.0.0 |
| 108 | * |
| 109 | * @var string |
| 110 | */ |
| 111 | protected $subtitle_esc_cb = 'esc_html'; |
| 112 | |
| 113 | /** |
| 114 | * Modal body content or template path. |
| 115 | * |
| 116 | * @since 4.0.0 |
| 117 | * |
| 118 | * @var string |
| 119 | */ |
| 120 | protected $body = ''; |
| 121 | |
| 122 | /** |
| 123 | * Modal body content esc callback. |
| 124 | * |
| 125 | * @since 4.0.0 |
| 126 | * |
| 127 | * @var string |
| 128 | */ |
| 129 | protected $body_esc_cb = 'wp_kses_post'; |
| 130 | |
| 131 | /** |
| 132 | * Whether body is a template path. |
| 133 | * |
| 134 | * @since 4.0.0 |
| 135 | * |
| 136 | * @var bool |
| 137 | */ |
| 138 | protected $is_template = false; |
| 139 | |
| 140 | /** |
| 141 | * Template data. |
| 142 | * |
| 143 | * @since 4.0.0 |
| 144 | * |
| 145 | * @var array |
| 146 | */ |
| 147 | protected $template_data = array(); |
| 148 | |
| 149 | /** |
| 150 | * Footer buttons HTML. |
| 151 | * |
| 152 | * @since 4.0.0 |
| 153 | * |
| 154 | * @var string |
| 155 | */ |
| 156 | protected $footer_buttons = ''; |
| 157 | |
| 158 | /** |
| 159 | * Footer alignment (left|center|right). |
| 160 | * |
| 161 | * @since 4.0.0 |
| 162 | * |
| 163 | * @var string |
| 164 | */ |
| 165 | protected $footer_alignment = 'right'; |
| 166 | |
| 167 | /** |
| 168 | * Whether modal is closeable. |
| 169 | * |
| 170 | * @since 4.0.0 |
| 171 | * |
| 172 | * @var bool |
| 173 | */ |
| 174 | protected $closeable = true; |
| 175 | |
| 176 | /** |
| 177 | * Initial modal state: 'open' or 'closed'. |
| 178 | * |
| 179 | * @since 4.0.0 |
| 180 | * |
| 181 | * @var string |
| 182 | */ |
| 183 | protected $state = 'closed'; |
| 184 | |
| 185 | /** |
| 186 | * Custom width for modal content. |
| 187 | * |
| 188 | * @since 4.0.0 |
| 189 | * |
| 190 | * @var string |
| 191 | */ |
| 192 | protected $width = ''; |
| 193 | |
| 194 | /** |
| 195 | * Set modal ID. |
| 196 | * |
| 197 | * @since 4.0.0 |
| 198 | * |
| 199 | * @param string $id Modal unique ID. |
| 200 | * |
| 201 | * @return $this |
| 202 | */ |
| 203 | public function id( $id ) { |
| 204 | $this->id = sanitize_key( $id ); |
| 205 | return $this; |
| 206 | } |
| 207 | |
| 208 | /** |
| 209 | * Set modal title. |
| 210 | * |
| 211 | * @since 4.0.0 |
| 212 | * |
| 213 | * @param string $title Modal title. |
| 214 | * @param string $esc_cb Callable escaping function. |
| 215 | * |
| 216 | * @return $this |
| 217 | */ |
| 218 | public function title( string $title, $esc_cb = 'esc_html' ) { |
| 219 | $this->title = $title; |
| 220 | $this->title_esc_cb = $esc_cb; |
| 221 | return $this; |
| 222 | } |
| 223 | |
| 224 | /** |
| 225 | * Set modal title icon. |
| 226 | * |
| 227 | * @since 4.0.0 |
| 228 | * |
| 229 | * @param string $title_icon the modal title icon name. |
| 230 | * |
| 231 | * @return $this |
| 232 | */ |
| 233 | public function title_icon( string $title_icon ) { |
| 234 | $this->title_icon = $title_icon; |
| 235 | return $this; |
| 236 | } |
| 237 | |
| 238 | /** |
| 239 | * Set modal subtitle. |
| 240 | * |
| 241 | * @since 4.0.0 |
| 242 | * |
| 243 | * @param string $subtitle Modal subtitle. |
| 244 | * @param string $esc_cb Callable escaping function. |
| 245 | * |
| 246 | * @return $this |
| 247 | */ |
| 248 | public function subtitle( string $subtitle, $esc_cb = 'esc_html' ) { |
| 249 | $this->subtitle = $subtitle; |
| 250 | $this->subtitle_esc_cb = $esc_cb; |
| 251 | return $this; |
| 252 | } |
| 253 | |
| 254 | /** |
| 255 | * Set modal body content. |
| 256 | * |
| 257 | * @since 4.0.0 |
| 258 | * |
| 259 | * @param string $body Body content HTML. |
| 260 | * @param string $esc_cb Body content HTML. |
| 261 | * |
| 262 | * @return $this |
| 263 | */ |
| 264 | public function body( $body, $esc_cb = 'wp_kses_post' ) { |
| 265 | $this->body = $body; |
| 266 | $this->body_esc_cb = $esc_cb; |
| 267 | $this->is_template = false; |
| 268 | return $this; |
| 269 | } |
| 270 | |
| 271 | /** |
| 272 | * Set modal body from template path. |
| 273 | * |
| 274 | * @since 4.0.0 |
| 275 | * |
| 276 | * @param string $path Template file path. |
| 277 | * @param array $data Template data. |
| 278 | * |
| 279 | * @return $this |
| 280 | */ |
| 281 | public function template( $path, $data = array() ) { |
| 282 | $this->body = $path; |
| 283 | $this->is_template = true; |
| 284 | $this->template_data = $data; |
| 285 | return $this; |
| 286 | } |
| 287 | |
| 288 | /** |
| 289 | * Set footer buttons HTML. |
| 290 | * |
| 291 | * @since 4.0.0 |
| 292 | * |
| 293 | * @param string $buttons Footer buttons HTML. |
| 294 | * |
| 295 | * @return $this |
| 296 | */ |
| 297 | public function footer_buttons( $buttons ) { |
| 298 | $this->footer_buttons = $buttons; |
| 299 | return $this; |
| 300 | } |
| 301 | |
| 302 | /** |
| 303 | * Set footer button alignment. |
| 304 | * |
| 305 | * @since 4.0.0 |
| 306 | * |
| 307 | * @param string $alignment Alignment (left|center|right). |
| 308 | * |
| 309 | * @return $this |
| 310 | */ |
| 311 | public function footer_alignment( $alignment ) { |
| 312 | $allowed = array( 'left', 'center', 'right' ); |
| 313 | if ( in_array( $alignment, $allowed, true ) ) { |
| 314 | $this->footer_alignment = $alignment; |
| 315 | } |
| 316 | return $this; |
| 317 | } |
| 318 | |
| 319 | /** |
| 320 | * Set whether modal is closeable. |
| 321 | * |
| 322 | * @since 4.0.0 |
| 323 | * |
| 324 | * @param bool $closeable Whether modal can be closed. |
| 325 | * |
| 326 | * @return $this |
| 327 | */ |
| 328 | public function closeable( $closeable = true ) { |
| 329 | $this->closeable = (bool) $closeable; |
| 330 | return $this; |
| 331 | } |
| 332 | |
| 333 | /** |
| 334 | * Set initial modal state (open or closed). |
| 335 | * |
| 336 | * @since 4.0.0 |
| 337 | * |
| 338 | * @param string $state Either 'open' or 'closed'. Default is 'closed'. |
| 339 | * |
| 340 | * @return $this |
| 341 | */ |
| 342 | public function state( string $state ) { |
| 343 | $this->state = 'open' === $state ? 'open' : 'closed'; |
| 344 | return $this; |
| 345 | } |
| 346 | |
| 347 | /** |
| 348 | * Set custom width for modal content. |
| 349 | * |
| 350 | * @since 4.0.0 |
| 351 | * |
| 352 | * @param string $width Width value (e.g., '354px', '50%'). |
| 353 | * |
| 354 | * @return $this |
| 355 | */ |
| 356 | public function width( $width ) { |
| 357 | $this->width = $width; |
| 358 | return $this; |
| 359 | } |
| 360 | |
| 361 | /** |
| 362 | * Render modal header. |
| 363 | * |
| 364 | * @since 4.0.0 |
| 365 | * |
| 366 | * @return string Header HTML. |
| 367 | */ |
| 368 | protected function render_header() { |
| 369 | if ( empty( $this->title ) && empty( $this->subtitle ) ) { |
| 370 | return ''; |
| 371 | } |
| 372 | |
| 373 | $icon = ''; |
| 374 | |
| 375 | $title_html = $this->title |
| 376 | ? sprintf( '<div class="tutor-modal-title">%s</div>', $this->esc( $this->title, $this->title_esc_cb ) ) |
| 377 | : ''; |
| 378 | |
| 379 | $subtitle_html = $this->subtitle |
| 380 | ? sprintf( '<div class="tutor-modal-subtitle">%s</div>', $this->esc( $this->subtitle, $this->subtitle_esc_cb ) ) |
| 381 | : ''; |
| 382 | |
| 383 | if ( ! empty( $this->title_icon ) ) { |
| 384 | ob_start(); |
| 385 | SvgIcon::make()->name( $this->title_icon )->size( 24 )->render(); |
| 386 | $icon = ob_get_clean(); |
| 387 | |
| 388 | return sprintf( |
| 389 | '<div class="tutor-modal-header"> |
| 390 | <div class="tutor-flex tutor-items-center tutor-gap-4"> |
| 391 | %3$s%1$s |
| 392 | </div> |
| 393 | %2$s |
| 394 | </div>', |
| 395 | $title_html, |
| 396 | $subtitle_html, |
| 397 | $icon, |
| 398 | ); |
| 399 | |
| 400 | } |
| 401 | |
| 402 | return sprintf( |
| 403 | '<div class="tutor-modal-header">%s%s</div>', |
| 404 | $title_html, |
| 405 | $subtitle_html |
| 406 | ); |
| 407 | } |
| 408 | |
| 409 | /** |
| 410 | * Render modal body. |
| 411 | * |
| 412 | * @since 4.0.0 |
| 413 | * |
| 414 | * @return string Body HTML. |
| 415 | */ |
| 416 | protected function render_body() { |
| 417 | if ( empty( $this->body ) ) { |
| 418 | return ''; |
| 419 | } |
| 420 | |
| 421 | $content = ''; |
| 422 | |
| 423 | if ( $this->is_template ) { |
| 424 | if ( file_exists( $this->body ) ) { |
| 425 | ob_start(); |
| 426 | $data = $this->template_data; |
| 427 | include $this->body; |
| 428 | $content = ob_get_clean(); |
| 429 | return $content ? $content : ''; |
| 430 | } |
| 431 | } else { |
| 432 | $content = $this->esc( $this->body, $this->body_esc_cb ); |
| 433 | } |
| 434 | |
| 435 | return sprintf( '<div class="tutor-modal-body">%s</div>', $content ); |
| 436 | } |
| 437 | |
| 438 | /** |
| 439 | * Render modal footer. |
| 440 | * |
| 441 | * @since 4.0.0 |
| 442 | * |
| 443 | * @return string Footer HTML. |
| 444 | */ |
| 445 | protected function render_footer() { |
| 446 | if ( empty( $this->footer_buttons ) ) { |
| 447 | return ''; |
| 448 | } |
| 449 | |
| 450 | $alignment_class = ''; |
| 451 | if ( 'left' === $this->footer_alignment ) { |
| 452 | $alignment_class = ' tutor-justify-start'; |
| 453 | } elseif ( 'center' === $this->footer_alignment ) { |
| 454 | $alignment_class = ' tutor-justify-center'; |
| 455 | } |
| 456 | |
| 457 | return sprintf( |
| 458 | '<div class="tutor-modal-footer%s">%s</div>', |
| 459 | esc_attr( $alignment_class ), |
| 460 | $this->footer_buttons |
| 461 | ); |
| 462 | } |
| 463 | |
| 464 | /** |
| 465 | * Get the modal HTML. |
| 466 | * |
| 467 | * @since 4.0.0 |
| 468 | * |
| 469 | * @return string HTML output. |
| 470 | */ |
| 471 | public function get(): string { |
| 472 | if ( empty( $this->id ) ) { |
| 473 | return ''; |
| 474 | } |
| 475 | |
| 476 | // Build Alpine.js x-data. |
| 477 | $alpine_data = array( |
| 478 | 'id' => $this->id, |
| 479 | 'initialOpen' => 'open' === $this->state, |
| 480 | ); |
| 481 | if ( ! $this->closeable ) { |
| 482 | $alpine_data['isCloseable'] = false; |
| 483 | } |
| 484 | |
| 485 | $alpine_json = wp_json_encode( $alpine_data ); |
| 486 | |
| 487 | // Build close button. |
| 488 | $close_button = $this->closeable |
| 489 | ? '<button x-data="tutorIcon({ name: \'cross\', width: 16, height: 16})" x-bind="getCloseButtonBindings()" aria-label="' . esc_attr__( 'Close', 'tutor' ) . '"></button>' |
| 490 | : ''; |
| 491 | |
| 492 | // Build style attribute for custom width. |
| 493 | $style_attr = $this->width |
| 494 | ? sprintf( ' style="max-width: %s;"', esc_attr( $this->width ) ) |
| 495 | : ''; |
| 496 | |
| 497 | // Build modal content. |
| 498 | $header = $this->render_header(); |
| 499 | $body = $this->render_body(); |
| 500 | $footer = $this->render_footer(); |
| 501 | |
| 502 | $this->component_string = sprintf( |
| 503 | '<div x-data="tutorModal(%s)" x-cloak style="display: none;"> |
| 504 | <template x-teleport="body"> |
| 505 | <div x-bind="getModalBindings()"> |
| 506 | <div x-bind="getBackdropBindings()"></div> |
| 507 | <div x-bind="getModalContentBindings()"%s> |
| 508 | %s |
| 509 | %s |
| 510 | %s |
| 511 | %s |
| 512 | </div> |
| 513 | </div> |
| 514 | </template> |
| 515 | </div>', |
| 516 | esc_attr( $alpine_json ), |
| 517 | $style_attr, |
| 518 | $close_button, |
| 519 | $header, |
| 520 | $body, |
| 521 | $footer |
| 522 | ); |
| 523 | |
| 524 | return $this->component_string; |
| 525 | } |
| 526 | } |
| 527 |