Constants
3 weeks ago
Accordion.php
1 week ago
Alert.php
1 week ago
AttachmentCard.php
1 week ago
Avatar.php
1 week ago
Badge.php
1 week ago
BaseComponent.php
3 weeks ago
Button.php
1 week ago
ConfirmationModal.php
1 week ago
CourseFilter.php
1 week ago
DateFilter.php
1 week ago
DropdownFilter.php
1 week ago
EmptyState.php
1 week ago
FileUploader.php
1 week ago
InputField.php
1 week ago
Modal.php
1 week ago
Nav.php
1 week ago
Pagination.php
1 week ago
Popover.php
1 week ago
PreviewTrigger.php
1 week ago
Progress.php
1 week ago
SearchFilter.php
1 week ago
Sorting.php
1 week ago
StarRating.php
1 week ago
StarRatingInput.php
1 week ago
StatusSelect.php
1 week ago
SvgIcon.php
1 week ago
Table.php
1 week ago
Tabs.php
1 week ago
Tooltip.php
1 week ago
WPEditor.php
1 week ago
WPEditor.php
485 lines
| 1 | <?php |
| 2 | /** |
| 3 | * Tutor Component: WPEditor |
| 4 | * |
| 5 | * Provides a fluent builder for rendering WordPress editor (TinyMCE/QuickTags) |
| 6 | * with tutorForm integration for validation and state management. |
| 7 | * |
| 8 | * @package Tutor\Components |
| 9 | * @author Themeum |
| 10 | * @link https://themeum.com |
| 11 | * @since 4.0.0 |
| 12 | */ |
| 13 | |
| 14 | namespace Tutor\Components; |
| 15 | |
| 16 | use Tutor\Helpers\UrlHelper; |
| 17 | |
| 18 | defined( 'ABSPATH' ) || exit; |
| 19 | |
| 20 | /** |
| 21 | * WPEditor Component Class. |
| 22 | * |
| 23 | * Example usage: |
| 24 | * ```php |
| 25 | * // Basic editor with label |
| 26 | * WPEditor::make() |
| 27 | * ->name( 'description' ) |
| 28 | * ->label( 'Course Description' ) |
| 29 | * ->content( 'Default content here.' ) |
| 30 | * ->render(); |
| 31 | * |
| 32 | * // Required editor with placeholder |
| 33 | * WPEditor::make() |
| 34 | * ->name( 'answer' ) |
| 35 | * ->label( 'Your Answer' ) |
| 36 | * ->placeholder( 'Write your answer here...' ) |
| 37 | * ->required( true ) |
| 38 | * ->render(); |
| 39 | * |
| 40 | * // Editor with Alpine.js form validation binding |
| 41 | * WPEditor::make() |
| 42 | * ->name( 'answer' ) |
| 43 | * ->label( 'Your Answer' ) |
| 44 | * ->attr( 'x-bind', "register('answer', { required: 'Answer is required' })" ) |
| 45 | * ->render(); |
| 46 | * |
| 47 | * // Editor pre-filled with saved content |
| 48 | * WPEditor::make() |
| 49 | * ->name( 'bio' ) |
| 50 | * ->label( 'Instructor Bio' ) |
| 51 | * ->content( get_user_meta( $user_id, 'description', true ) ) |
| 52 | * ->render(); |
| 53 | * |
| 54 | * // Editor with help text and error state |
| 55 | * WPEditor::make() |
| 56 | * ->name( 'overview' ) |
| 57 | * ->label( 'Course Overview' ) |
| 58 | * ->help_text( 'Describe what students will learn.' ) |
| 59 | * ->error( 'This field is required.' ) |
| 60 | * ->render(); |
| 61 | * |
| 62 | * // Custom TinyMCE toolbar (no media buttons, custom toolbar) |
| 63 | * WPEditor::make() |
| 64 | * ->name( 'announcement' ) |
| 65 | * ->label( 'Announcement' ) |
| 66 | * ->editor_config( array( |
| 67 | * 'teeny' => false, |
| 68 | * 'media_buttons' => false, |
| 69 | * 'quicktags' => false, |
| 70 | * 'editor_height' => 200, |
| 71 | * 'tinymce' => array( |
| 72 | * 'toolbar1' => 'bold,italic,underline,link,unlink,bullist,numlist,removeformat', |
| 73 | * 'toolbar2' => '', |
| 74 | * 'toolbar3' => '', |
| 75 | * ), |
| 76 | * ) ) |
| 77 | * ->render(); |
| 78 | * |
| 79 | * // Teeny editor (minimal toolbar) |
| 80 | * WPEditor::make() |
| 81 | * ->name( 'short_bio' ) |
| 82 | * ->label( 'Short Bio' ) |
| 83 | * ->editor_config( array( 'teeny' => true, 'media_buttons' => false ) ) |
| 84 | * ->render(); |
| 85 | * |
| 86 | * // Custom wrapper attribute (e.g., Alpine.js show binding) |
| 87 | * WPEditor::make() |
| 88 | * ->name( 'notes' ) |
| 89 | * ->label( 'Notes' ) |
| 90 | * ->wrapper_attr( 'x-show', 'showNotes' ) |
| 91 | * ->render(); |
| 92 | * |
| 93 | * // With explicit editor ID |
| 94 | * WPEditor::make() |
| 95 | * ->name( 'content' ) |
| 96 | * ->id( 'my-custom-editor-id' ) |
| 97 | * ->render(); |
| 98 | * |
| 99 | * // Retrieve HTML without echoing |
| 100 | * $html = WPEditor::make()->name( 'description' )->content( $saved_content )->get(); |
| 101 | * ``` |
| 102 | * |
| 103 | * @since 4.0.0 |
| 104 | */ |
| 105 | class WPEditor extends BaseComponent { |
| 106 | |
| 107 | /** |
| 108 | * Editor name attribute (also used as editor ID if ID is not set). |
| 109 | * |
| 110 | * @since 4.0.0 |
| 111 | * |
| 112 | * @var string |
| 113 | */ |
| 114 | protected $name = ''; |
| 115 | |
| 116 | /** |
| 117 | * Editor ID attribute. |
| 118 | * |
| 119 | * @since 4.0.0 |
| 120 | * |
| 121 | * @var string |
| 122 | */ |
| 123 | protected $id = ''; |
| 124 | |
| 125 | /** |
| 126 | * Editor content. |
| 127 | * |
| 128 | * @since 4.0.0 |
| 129 | * |
| 130 | * @var string |
| 131 | */ |
| 132 | protected $content = ''; |
| 133 | |
| 134 | /** |
| 135 | * Editor label text. |
| 136 | * |
| 137 | * @since 4.0.0 |
| 138 | * |
| 139 | * @var string |
| 140 | */ |
| 141 | protected $label = ''; |
| 142 | |
| 143 | /** |
| 144 | * Editor placeholder text. |
| 145 | * |
| 146 | * @since 4.0.0 |
| 147 | * |
| 148 | * @var string |
| 149 | */ |
| 150 | protected $placeholder = ''; |
| 151 | |
| 152 | /** |
| 153 | * Help text below editor. |
| 154 | * |
| 155 | * @since 4.0.0 |
| 156 | * |
| 157 | * @var string |
| 158 | */ |
| 159 | protected $help_text = ''; |
| 160 | |
| 161 | /** |
| 162 | * Error message text. |
| 163 | * |
| 164 | * @since 4.0.0 |
| 165 | * |
| 166 | * @var string |
| 167 | */ |
| 168 | protected $error = ''; |
| 169 | |
| 170 | /** |
| 171 | * Whether editor is required. |
| 172 | * |
| 173 | * @since 4.0.0 |
| 174 | * |
| 175 | * @var bool |
| 176 | */ |
| 177 | protected $required = false; |
| 178 | |
| 179 | /** |
| 180 | * WordPress editor configuration. |
| 181 | * |
| 182 | * @since 4.0.0 |
| 183 | * |
| 184 | * @var array |
| 185 | */ |
| 186 | protected $editor_config = array(); |
| 187 | |
| 188 | /** |
| 189 | * Set editor name. |
| 190 | * |
| 191 | * @since 4.0.0 |
| 192 | * |
| 193 | * @param string $name Editor name. |
| 194 | * |
| 195 | * @return $this |
| 196 | */ |
| 197 | public function name( $name ) { |
| 198 | $this->name = sanitize_key( $name ); |
| 199 | return $this; |
| 200 | } |
| 201 | |
| 202 | /** |
| 203 | * Set editor ID. |
| 204 | * |
| 205 | * @since 4.0.0 |
| 206 | * |
| 207 | * @param string $id Editor ID. |
| 208 | * |
| 209 | * @return $this |
| 210 | */ |
| 211 | public function id( $id ) { |
| 212 | $this->id = sanitize_key( $id ); |
| 213 | return $this; |
| 214 | } |
| 215 | |
| 216 | /** |
| 217 | * Set editor content. |
| 218 | * |
| 219 | * @since 4.0.0 |
| 220 | * |
| 221 | * @param string $content Editor content. |
| 222 | * |
| 223 | * @return $this |
| 224 | */ |
| 225 | public function content( $content ) { |
| 226 | $this->content = $content; |
| 227 | return $this; |
| 228 | } |
| 229 | |
| 230 | /** |
| 231 | * Set editor label. |
| 232 | * |
| 233 | * @since 4.0.0 |
| 234 | * |
| 235 | * @param string $label Label text. |
| 236 | * |
| 237 | * @return $this |
| 238 | */ |
| 239 | public function label( $label ) { |
| 240 | $this->label = $label; |
| 241 | return $this; |
| 242 | } |
| 243 | |
| 244 | /** |
| 245 | * Set editor placeholder. |
| 246 | * |
| 247 | * @since 4.0.0 |
| 248 | * |
| 249 | * @param string $placeholder Placeholder text. |
| 250 | * |
| 251 | * @return $this |
| 252 | */ |
| 253 | public function placeholder( $placeholder ) { |
| 254 | $this->placeholder = $placeholder; |
| 255 | return $this; |
| 256 | } |
| 257 | |
| 258 | /** |
| 259 | * Set help text. |
| 260 | * |
| 261 | * @since 4.0.0 |
| 262 | * |
| 263 | * @param string $text Help text. |
| 264 | * |
| 265 | * @return $this |
| 266 | */ |
| 267 | public function help_text( $text ) { |
| 268 | $this->help_text = $text; |
| 269 | return $this; |
| 270 | } |
| 271 | |
| 272 | /** |
| 273 | * Set error message. |
| 274 | * |
| 275 | * @since 4.0.0 |
| 276 | * |
| 277 | * @param string $error Error message. |
| 278 | * |
| 279 | * @return $this |
| 280 | */ |
| 281 | public function error( $error ) { |
| 282 | $this->error = $error; |
| 283 | return $this; |
| 284 | } |
| 285 | |
| 286 | /** |
| 287 | * Set required state. |
| 288 | * |
| 289 | * @since 4.0.0 |
| 290 | * |
| 291 | * @param bool $required Whether editor is required. |
| 292 | * |
| 293 | * @return $this |
| 294 | */ |
| 295 | public function required( $required = true ) { |
| 296 | $this->required = $required; |
| 297 | return $this; |
| 298 | } |
| 299 | |
| 300 | /** |
| 301 | * Set WordPress editor configuration. |
| 302 | * |
| 303 | * @since 4.0.0 |
| 304 | * |
| 305 | * @param array $config Editor configuration. |
| 306 | * |
| 307 | * @return $this |
| 308 | */ |
| 309 | public function editor_config( $config ) { |
| 310 | $this->editor_config = $config; |
| 311 | return $this; |
| 312 | } |
| 313 | |
| 314 | /** |
| 315 | * Get default editor configuration. |
| 316 | * |
| 317 | * @since 4.0.0 |
| 318 | * |
| 319 | * @return array |
| 320 | */ |
| 321 | protected function get_default_config() { |
| 322 | return array( |
| 323 | 'media_buttons' => false, |
| 324 | 'teeny' => true, |
| 325 | 'quicktags' => true, |
| 326 | 'editor_height' => 150, |
| 327 | 'textarea_name' => $this->name, |
| 328 | 'tinymce' => array( |
| 329 | 'skin' => 'light', |
| 330 | 'skin_url' => UrlHelper::asset( 'lib/tinymce/light' ), |
| 331 | 'content_css' => $this->get_content_css(), |
| 332 | ), |
| 333 | ); |
| 334 | } |
| 335 | |
| 336 | /** |
| 337 | * Get TinyMCE content styles used inside the editor iframe. |
| 338 | * |
| 339 | * Mirrors the stylesheet list used by the React WPEditor component so both |
| 340 | * entry points render editor content consistently. |
| 341 | * |
| 342 | * @since 4.0.0 |
| 343 | * |
| 344 | * @return string |
| 345 | */ |
| 346 | protected function get_content_css() { |
| 347 | return implode( |
| 348 | ',', |
| 349 | array( |
| 350 | includes_url( 'css/dashicons.min.css' ), |
| 351 | includes_url( 'js/tinymce/skins/wordpress/wp-content.css' ), |
| 352 | UrlHelper::asset( 'lib/tinymce/light/content.min.css' ), |
| 353 | ) |
| 354 | ); |
| 355 | } |
| 356 | |
| 357 | /** |
| 358 | * Attributes for the wrapper element. |
| 359 | * |
| 360 | * @since 4.0.0 |
| 361 | * |
| 362 | * @var array |
| 363 | */ |
| 364 | protected $wrapper_attr = array(); |
| 365 | |
| 366 | /** |
| 367 | * Set wrapper attribute. |
| 368 | * |
| 369 | * @since 4.0.0 |
| 370 | * |
| 371 | * @param string $key Attribute key. |
| 372 | * @param string $value Attribute value. |
| 373 | * |
| 374 | * @return $this |
| 375 | */ |
| 376 | public function wrapper_attr( $key, $value ) { |
| 377 | $this->wrapper_attr[ $key ] = $value; |
| 378 | return $this; |
| 379 | } |
| 380 | |
| 381 | /** |
| 382 | * Get the editor component as HTML string. |
| 383 | * |
| 384 | * @since 4.0.0 |
| 385 | * |
| 386 | * @return string Component HTML. |
| 387 | */ |
| 388 | public function get(): string { |
| 389 | if ( empty( $this->name ) ) { |
| 390 | return ''; |
| 391 | } |
| 392 | |
| 393 | ob_start(); |
| 394 | |
| 395 | $editor_id = ! empty( $this->id ) ? $this->id : $this->name; |
| 396 | $default_config = $this->get_default_config(); |
| 397 | $config = array_merge( $default_config, $this->editor_config ); |
| 398 | |
| 399 | if ( isset( $default_config['tinymce'] ) || isset( $this->editor_config['tinymce'] ) ) { |
| 400 | $config['tinymce'] = array_merge( |
| 401 | $default_config['tinymce'] ?? array(), |
| 402 | $this->editor_config['tinymce'] ?? array() |
| 403 | ); |
| 404 | } |
| 405 | $wrapper_class = 'tutor-wp-editor-wrapper tutor-input-field'; |
| 406 | |
| 407 | if ( isset( $this->attributes['class'] ) ) { |
| 408 | $wrapper_class .= ' ' . $this->attributes['class']; |
| 409 | } |
| 410 | if ( isset( $this->wrapper_attr['class'] ) ) { |
| 411 | $wrapper_class .= ' ' . $this->wrapper_attr['class']; |
| 412 | } |
| 413 | |
| 414 | if ( ! empty( $this->error ) ) { |
| 415 | $wrapper_class .= ' tutor-wp-editor-error tutor-input-field-error'; |
| 416 | } |
| 417 | |
| 418 | $wrapper_attrs = $this->wrapper_attr; |
| 419 | |
| 420 | $wrapper_attrs['class'] = $wrapper_class; |
| 421 | |
| 422 | ?> |
| 423 | <div |
| 424 | <?php |
| 425 | foreach ( $wrapper_attrs as $key => $value ) { |
| 426 | printf( '%s="%s"', esc_attr( $key ), esc_attr( $value ) ); |
| 427 | } |
| 428 | ?> |
| 429 | x-data="tutorWPEditor({ |
| 430 | name: '<?php echo esc_js( $this->name ); ?>', |
| 431 | editorId: '<?php echo esc_js( $editor_id ); ?>', |
| 432 | placeholder: '<?php echo esc_js( $this->placeholder ); ?>' |
| 433 | })" |
| 434 | > |
| 435 | <?php if ( ! empty( $this->label ) ) : ?> |
| 436 | <label |
| 437 | for="<?php echo esc_attr( $editor_id ); ?>" |
| 438 | class="tutor-label <?php echo $this->required ? 'tutor-label-required' : ''; ?>" |
| 439 | > |
| 440 | <?php echo esc_html( $this->label ); ?> |
| 441 | </label> |
| 442 | <?php endif; ?> |
| 443 | |
| 444 | <div class="tutor-wp-editor-container"> |
| 445 | <!-- Hidden input for form value binding --> |
| 446 | <input |
| 447 | type="hidden" |
| 448 | name="<?php echo esc_attr( $this->name ); ?>" |
| 449 | <?php $this->render_attributes(); ?> |
| 450 | /> |
| 451 | |
| 452 | <?php |
| 453 | // Render WordPress editor. |
| 454 | wp_editor( $this->content, $editor_id, $config ); |
| 455 | ?> |
| 456 | </div> |
| 457 | |
| 458 | <!-- Error Text (Alpine + PHP) --> |
| 459 | <div |
| 460 | class="tutor-error-text" |
| 461 | x-cloak |
| 462 | x-show="errors?.['<?php echo esc_js( $this->name ); ?>']" |
| 463 | x-text="errors?.['<?php echo esc_js( $this->name ); ?>']?.message" |
| 464 | role="alert" |
| 465 | aria-live="polite" |
| 466 | > |
| 467 | <?php echo esc_html( $this->error ); ?> |
| 468 | </div> |
| 469 | |
| 470 | <!-- Help Text --> |
| 471 | <?php if ( ! empty( $this->help_text ) ) : ?> |
| 472 | <div |
| 473 | class="tutor-help-text" |
| 474 | x-show="!errors?.['<?php echo esc_js( $this->name ); ?>']?.message" |
| 475 | > |
| 476 | <?php echo esc_html( $this->help_text ); ?> |
| 477 | </div> |
| 478 | <?php endif; ?> |
| 479 | </div> |
| 480 | <?php |
| 481 | |
| 482 | return ob_get_clean(); |
| 483 | } |
| 484 | } |
| 485 |