class-settings-builder.php
2266 lines
| 1 | <?php |
| 2 | /** |
| 3 | * Class: Determine the context in which the plugin is executed. |
| 4 | * |
| 5 | * Helper class to determine the proper status of the request. |
| 6 | * |
| 7 | * @package wp-2fa |
| 8 | * |
| 9 | * @since 4.0.0 |
| 10 | */ |
| 11 | |
| 12 | // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped |
| 13 | |
| 14 | declare(strict_types=1); |
| 15 | |
| 16 | namespace WP2FA\Admin; |
| 17 | |
| 18 | use WP2FA\Utils\Settings_Utils; |
| 19 | |
| 20 | // Exit if accessed directly. |
| 21 | if ( ! defined( 'ABSPATH' ) ) { |
| 22 | exit; |
| 23 | } |
| 24 | |
| 25 | /** |
| 26 | * Upgrade notice class |
| 27 | */ |
| 28 | if ( ! class_exists( '\WP2FA\Admin\Settings_Builder' ) ) { |
| 29 | /** |
| 30 | * Utility class for showing the upgrade notice in the plugins page. |
| 31 | * |
| 32 | * @package wp-2fa |
| 33 | * |
| 34 | * @since 4.0.0 |
| 35 | */ |
| 36 | class Settings_Builder { |
| 37 | /** |
| 38 | * Whether the multi-select-ajax CSS/JS assets have been output. |
| 39 | * |
| 40 | * @var bool |
| 41 | * |
| 42 | * @since 4.0.0 |
| 43 | */ |
| 44 | private static $multi_select_ajax_rendered = false; |
| 45 | |
| 46 | /** |
| 47 | * The inner item id of the setting |
| 48 | * |
| 49 | * @var string |
| 50 | * |
| 51 | * @since 4.0.0 |
| 52 | */ |
| 53 | public static $item_id; |
| 54 | |
| 55 | /** |
| 56 | * Additional attributes of the item. |
| 57 | * |
| 58 | * @var string |
| 59 | * |
| 60 | * @since 4.0.0 |
| 61 | */ |
| 62 | public static $item_id_attr; |
| 63 | |
| 64 | /** |
| 65 | * Item wrapper - the id of the setting + '_item' suffix |
| 66 | * |
| 67 | * @var string |
| 68 | * |
| 69 | * @since 4.0.0 |
| 70 | */ |
| 71 | public static $item_id_wrap; |
| 72 | |
| 73 | /** |
| 74 | * The name attribute of the element |
| 75 | * |
| 76 | * @var string |
| 77 | * |
| 78 | * @since 4.0.0 |
| 79 | */ |
| 80 | public static $name_attr; |
| 81 | |
| 82 | /** |
| 83 | * Placeholder for the setting |
| 84 | * |
| 85 | * @var string |
| 86 | * |
| 87 | * @since 4.0.0 |
| 88 | */ |
| 89 | public static $placeholder_attr; |
| 90 | |
| 91 | /** |
| 92 | * Custom class for the setting |
| 93 | * |
| 94 | * @var string |
| 95 | * |
| 96 | * @since 4.0.0 |
| 97 | */ |
| 98 | public static $custom_class; |
| 99 | |
| 100 | /** |
| 101 | * The setting value |
| 102 | * |
| 103 | * @var mixed |
| 104 | * |
| 105 | * @since 4.0.0 |
| 106 | */ |
| 107 | public static $current_value; |
| 108 | |
| 109 | /** |
| 110 | * The type of the setting |
| 111 | * |
| 112 | * @var string |
| 113 | * |
| 114 | * @since 4.0.0 |
| 115 | */ |
| 116 | public static $option_type; |
| 117 | |
| 118 | /** |
| 119 | * The name of the setting |
| 120 | * |
| 121 | * @var string |
| 122 | * |
| 123 | * @since 4.0.0 |
| 124 | */ |
| 125 | public static $option_name; |
| 126 | |
| 127 | /** |
| 128 | * Array with the setting settings |
| 129 | * |
| 130 | * @var array |
| 131 | * |
| 132 | * @since 4.0.0 |
| 133 | */ |
| 134 | public static $settings; |
| 135 | |
| 136 | /** |
| 137 | * Array with the data attributes settings |
| 138 | * |
| 139 | * @var array |
| 140 | * |
| 141 | * @since 4.0.0 |
| 142 | */ |
| 143 | public static $data_attrs; |
| 144 | |
| 145 | /** |
| 146 | * Integer - minimum value |
| 147 | * |
| 148 | * @var int |
| 149 | * |
| 150 | * @since 4.0.0 |
| 151 | */ |
| 152 | public static $min; |
| 153 | |
| 154 | /** |
| 155 | * Integer - maximum value |
| 156 | * |
| 157 | * @var int |
| 158 | * |
| 159 | * @since 4.0.0 |
| 160 | */ |
| 161 | public static $max; |
| 162 | |
| 163 | /** |
| 164 | * Integer - step value |
| 165 | * |
| 166 | * @var int |
| 167 | * |
| 168 | * @since 4.0.0 |
| 169 | */ |
| 170 | public static $step; |
| 171 | |
| 172 | /** |
| 173 | * Holds the type of the edit HTML field |
| 174 | * |
| 175 | * @var string |
| 176 | * |
| 177 | * @since 4.0.0 |
| 178 | */ |
| 179 | public static $edit_type; |
| 180 | |
| 181 | /** |
| 182 | * Holds the validation pattern for the text field |
| 183 | * |
| 184 | * @var string |
| 185 | * |
| 186 | * @since 4.0.0 |
| 187 | */ |
| 188 | public static $validate_pattern; |
| 189 | |
| 190 | /** |
| 191 | * Holds the maximum characters for the text field |
| 192 | * |
| 193 | * @var string |
| 194 | * |
| 195 | * @since 4.0.0 |
| 196 | */ |
| 197 | public static $max_chars; |
| 198 | |
| 199 | /** |
| 200 | * Holds the title attribute for the text field |
| 201 | * |
| 202 | * @var string |
| 203 | * |
| 204 | * @since 4.0.0 |
| 205 | */ |
| 206 | public static $title_attr; |
| 207 | |
| 208 | /** |
| 209 | * The given field is required |
| 210 | * |
| 211 | * @var string |
| 212 | * |
| 213 | * @since 4.0.0 |
| 214 | */ |
| 215 | public static $required; |
| 216 | |
| 217 | /** |
| 218 | * If the value is not a boolean, for checkboxes. |
| 219 | * |
| 220 | * @var string |
| 221 | * |
| 222 | * @since 4.0.0 |
| 223 | */ |
| 224 | public static $not_bool; |
| 225 | |
| 226 | /** |
| 227 | * Builds the element |
| 228 | * |
| 229 | * @param array $settings - Array with settings. |
| 230 | * @param mixed $data - The data to show. |
| 231 | * |
| 232 | * @return void |
| 233 | * |
| 234 | * @since 4.0.0 |
| 235 | */ |
| 236 | public static function create( $settings, $data ) { |
| 237 | |
| 238 | self::$item_id = null; |
| 239 | self::$item_id_attr = null; |
| 240 | self::$item_id_wrap = null; |
| 241 | self::$name_attr = null; |
| 242 | self::$placeholder_attr = null; |
| 243 | self::$custom_class = null; |
| 244 | self::$current_value = null; |
| 245 | self::$option_type = null; |
| 246 | self::$option_name = null; |
| 247 | self::$settings = null; |
| 248 | self::$edit_type = null; |
| 249 | self::$validate_pattern = null; |
| 250 | self::$max_chars = null; |
| 251 | self::$min = null; |
| 252 | self::$max = null; |
| 253 | self::$step = null; |
| 254 | self::$data_attrs = null; |
| 255 | self::$not_bool = null; |
| 256 | |
| 257 | self::prepare_data( $settings, $data ); |
| 258 | |
| 259 | if ( empty( self::$option_type ) ) { |
| 260 | return; |
| 261 | } |
| 262 | |
| 263 | // Options Without Labels. |
| 264 | $with_label = false; |
| 265 | |
| 266 | switch ( self::$option_type ) { |
| 267 | case 'simple-text': |
| 268 | self::simple_text(); |
| 269 | break; |
| 270 | |
| 271 | case 'number': |
| 272 | self::number(); |
| 273 | break; |
| 274 | |
| 275 | case 'text': |
| 276 | self::text(); |
| 277 | break; |
| 278 | |
| 279 | case 'secret': |
| 280 | self::secret(); |
| 281 | break; |
| 282 | |
| 283 | case 'button': |
| 284 | self::button(); |
| 285 | break; |
| 286 | |
| 287 | case 'tab-title': |
| 288 | self::tab_title(); |
| 289 | break; |
| 290 | |
| 291 | case 'section-title': |
| 292 | self::section_title(); |
| 293 | break; |
| 294 | |
| 295 | case 'section-sub-title': |
| 296 | self::section_sub_title(); |
| 297 | break; |
| 298 | |
| 299 | case 'list': |
| 300 | self::list(); |
| 301 | break; |
| 302 | |
| 303 | case 'radio': |
| 304 | self::radio(); |
| 305 | break; |
| 306 | |
| 307 | case 'group-title': |
| 308 | self::group_title(); |
| 309 | break; |
| 310 | |
| 311 | case 'description': |
| 312 | self::description(); |
| 313 | break; |
| 314 | |
| 315 | case 'checkbox': |
| 316 | self::checkbox(); |
| 317 | break; |
| 318 | |
| 319 | case 'settings-label': |
| 320 | self::settings_label(); |
| 321 | break; |
| 322 | |
| 323 | case 'tip': |
| 324 | self::tip(); |
| 325 | break; |
| 326 | |
| 327 | case 'editor': |
| 328 | self::editor(); |
| 329 | break; |
| 330 | |
| 331 | case 'textarea': |
| 332 | self::textarea(); |
| 333 | break; |
| 334 | |
| 335 | case 'color': |
| 336 | self::color(); |
| 337 | break; |
| 338 | |
| 339 | case 'image-select': |
| 340 | self::image_select(); |
| 341 | break; |
| 342 | |
| 343 | case 'font-select': |
| 344 | self::font_select(); |
| 345 | break; |
| 346 | |
| 347 | case 'tabbed-navigation': |
| 348 | self::tabbed_navigation(); |
| 349 | break; |
| 350 | |
| 351 | case 'toggle-checkbox': |
| 352 | self::toggle_checkbox(); |
| 353 | break; |
| 354 | |
| 355 | case 'user-roles': |
| 356 | self::user_roles(); |
| 357 | break; |
| 358 | |
| 359 | case 'stat-cards': |
| 360 | self::stat_cards(); |
| 361 | break; |
| 362 | |
| 363 | case 'data-table': |
| 364 | self::data_table(); |
| 365 | break; |
| 366 | |
| 367 | case 'multi-select-ajax': |
| 368 | self::multi_select_ajax(); |
| 369 | break; |
| 370 | |
| 371 | case 'sortable-list': |
| 372 | self::sortable_list(); |
| 373 | break; |
| 374 | |
| 375 | case 'select': |
| 376 | self::select(); |
| 377 | break; |
| 378 | |
| 379 | case 'url-prefixed-text': |
| 380 | self::url_prefixed_text(); |
| 381 | break; |
| 382 | |
| 383 | case 'breadcrumb': |
| 384 | self::breadcrumb(); |
| 385 | break; |
| 386 | } |
| 387 | |
| 388 | self::hint(); |
| 389 | } |
| 390 | |
| 391 | /** |
| 392 | * Textarea |
| 393 | * |
| 394 | * @since 4.0.0 |
| 395 | */ |
| 396 | private static function textarea() { |
| 397 | ?> |
| 398 | <div class="body-editor-inner"> |
| 399 | <div class="editor-area"> |
| 400 | <?php |
| 401 | $textarea_value = self::$current_value; |
| 402 | if ( \is_array( $textarea_value ) ) { |
| 403 | $textarea_value = \implode( "\n", $textarea_value ); |
| 404 | } |
| 405 | ?> |
| 406 | <textarea style="width: 100%;" <?php echo self::$item_id_attr; ?> <?php echo self::$name_attr; ?> |
| 407 | rows="5"><?php echo \esc_textarea( (string) $textarea_value ); ?></textarea> |
| 408 | </div><!-- /.editor-area --> |
| 409 | <?php |
| 410 | if ( ! empty( self::$settings['legend'] ) ) { |
| 411 | ?> |
| 412 | <div class="token-list" aria-label="Available tokens"> |
| 413 | <div class="token-row"> |
| 414 | <?php |
| 415 | foreach ( self::$settings['legend'] as $item ) { |
| 416 | ?> |
| 417 | <span class="token-badge"><?php echo esc_html( $item ); ?></span> |
| 418 | <?php |
| 419 | } |
| 420 | ?> |
| 421 | </div> |
| 422 | </div> |
| 423 | <?php |
| 424 | } |
| 425 | ?> |
| 426 | </div> |
| 427 | <?php |
| 428 | } |
| 429 | |
| 430 | /** |
| 431 | * Tabbed Navigation |
| 432 | * |
| 433 | * @since 4.0.0 |
| 434 | */ |
| 435 | private static function tabbed_navigation() { |
| 436 | $styles = ''; |
| 437 | $labels = ''; |
| 438 | $radios = ''; |
| 439 | |
| 440 | $lock_icon = '<svg class="wp2fa-tab-lock-icon" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">' |
| 441 | . '<path d="M4.66699 6.41667V4.08333C4.66699 3.46449 4.91282 2.871 5.35041 2.43342C5.78799 1.99583 6.38149 1.75 7.00033 1.75C7.61916 1.75 8.21266 1.99583 8.65024 2.43342C9.08783 2.871 9.33366 3.46449 9.33366 4.08333V6.41667M2.91699 7.58333C2.91699 7.27391 3.03991 6.97717 3.2587 6.75838C3.47749 6.53958 3.77424 6.41667 4.08366 6.41667H9.91699C10.2264 6.41667 10.5232 6.53958 10.742 6.75838C10.9607 6.97717 11.0837 7.27391 11.0837 7.58333V11.0833C11.0837 11.3928 10.9607 11.6895 10.742 11.9083C10.5232 12.1271 10.2264 12.25 9.91699 12.25H4.08366C3.77424 12.25 3.47749 12.1271 3.2587 11.9083C3.03991 11.6895 2.91699 11.3928 2.91699 11.0833V7.58333ZM6.41699 9.33333C6.41699 9.48804 6.47845 9.63642 6.58785 9.74581C6.69724 9.85521 6.84562 9.91667 7.00033 9.91667C7.15504 9.91667 7.30341 9.85521 7.4128 9.74581C7.5222 9.63642 7.58366 9.48804 7.58366 9.33333C7.58366 9.17862 7.5222 9.03025 7.4128 8.92085C7.30341 8.81146 7.15504 8.75 7.00033 8.75C6.84562 8.75 6.69724 8.81146 6.58785 8.92085C6.47845 9.03025 6.41699 9.17862 6.41699 9.33333Z" stroke="#646970" stroke-width="1.3125" stroke-linecap="round" stroke-linejoin="round"/>' |
| 442 | . '</svg>'; |
| 443 | |
| 444 | $i = 0; |
| 445 | foreach ( self::$current_value as $value ) { |
| 446 | $is_locked = ! empty( $value['locked'] ); |
| 447 | |
| 448 | if ( $is_locked ) { |
| 449 | $context_attr = ''; |
| 450 | if ( ! empty( $value['premium_context'] ) ) { |
| 451 | $context_attr = ' data-wp2fa-premium-context="' . \esc_attr( $value['premium_context'] ) . '"'; |
| 452 | } |
| 453 | $labels .= '<span class="tab-label tab-label--locked wp2fa-premium-badge" role="button" tabindex="0"' . $context_attr . '>' . \esc_html( $value['title'] ) . ' ' . $lock_icon . '</span>'; |
| 454 | } else { |
| 455 | ++$i; |
| 456 | $checked = ( 1 === $i ) ? ' checked' : ''; |
| 457 | $styles .= '#tab-' . $value['id'] . ':checked ~ .tab-panel-' . $value['id'] . ' { display: block; } '; |
| 458 | $styles .= '#tab-' . $value['id'] . ':checked ~ .tab-nav .tab-label[for="tab-' . $value['id'] . '"] { color: var(--blue-color); border-bottom-color: var(--blue-color); } '; |
| 459 | $labels .= '<label class="tab-label" for="tab-' . $value['id'] . '">' . \esc_html( $value['title'] ) . '</label>'; |
| 460 | $radios .= '<input class="tab-radio" type="radio" id="tab-' . $value['id'] . '" name="' . self::$option_name . '"' . $checked . '>'; |
| 461 | } |
| 462 | } |
| 463 | ?> |
| 464 | <style> |
| 465 | <?php echo $styles; ?> |
| 466 | .tab-label--locked { |
| 467 | cursor: pointer; |
| 468 | opacity: 0.7; |
| 469 | display: inline-flex; |
| 470 | align-items: center; |
| 471 | gap: 4px; |
| 472 | } |
| 473 | .wp2fa-tab-lock-icon { |
| 474 | flex-shrink: 0; |
| 475 | } |
| 476 | </style> |
| 477 | <?php echo $radios; ?> |
| 478 | <nav class="tab-nav" aria-label="Template sections"> |
| 479 | <?php echo $labels; ?> |
| 480 | </nav> |
| 481 | |
| 482 | <?php |
| 483 | } |
| 484 | |
| 485 | /** |
| 486 | * Editor |
| 487 | * |
| 488 | * @since 4.0.0 |
| 489 | */ |
| 490 | private static function editor() { |
| 491 | ?> |
| 492 | <div class="body-editor-inner"> |
| 493 | <?php |
| 494 | |
| 495 | // Settings. |
| 496 | $settings = ! empty( self::$settings['editor'] ) ? self::$settings['editor'] : array( |
| 497 | 'editor_height' => '320', |
| 498 | 'media_buttons' => false, |
| 499 | ); |
| 500 | $settings['textarea_name'] = self::$option_name; |
| 501 | |
| 502 | self::$current_value = ! empty( self::$settings['kses'] ) ? wp_kses_stripslashes( stripslashes( self::$current_value ) ) : self::$current_value; |
| 503 | |
| 504 | if ( null === self::$current_value ) { |
| 505 | self::$current_value = ''; |
| 506 | } |
| 507 | |
| 508 | ?> |
| 509 | <!-- Editor (Visual / Code) --> |
| 510 | <div class="editor-area"> |
| 511 | <?php |
| 512 | |
| 513 | \wp_editor( |
| 514 | self::$current_value, |
| 515 | self::$item_id, |
| 516 | $settings |
| 517 | ); |
| 518 | ?> |
| 519 | </div><!-- /.editor-area --> |
| 520 | <?php |
| 521 | if ( ! empty( self::$settings['legend'] ) ) { |
| 522 | ?> |
| 523 | <div class="token-list" aria-label="Available tokens"> |
| 524 | <div class="token-row"> |
| 525 | <?php |
| 526 | foreach ( self::$settings['legend'] as $item ) { |
| 527 | ?> |
| 528 | <span class="token-badge"><?php echo esc_html( $item ); ?></span> |
| 529 | <?php |
| 530 | } |
| 531 | ?> |
| 532 | </div> |
| 533 | </div> |
| 534 | <?php |
| 535 | } |
| 536 | ?> |
| 537 | </div> |
| 538 | <?php |
| 539 | } |
| 540 | |
| 541 | /** |
| 542 | * Tip Message |
| 543 | * |
| 544 | * @since 4.0.0 |
| 545 | */ |
| 546 | private static function tip() { |
| 547 | if ( ! empty( self::$settings['text'] ) ) { |
| 548 | ?> |
| 549 | <div class="notice-tip"> |
| 550 | <div class="notice-tip-body"> |
| 551 | <strong><?php echo \esc_html__( 'Tip', 'wp-2fa' ); ?></strong> |
| 552 | <?php echo self::$settings['text']; ?> |
| 553 | </div> |
| 554 | </div> |
| 555 | <?php |
| 556 | } |
| 557 | } |
| 558 | |
| 559 | /** |
| 560 | * Setting Description |
| 561 | * |
| 562 | * @since 4.0.0 |
| 563 | */ |
| 564 | private static function hint() { |
| 565 | |
| 566 | if ( ! empty( self::$settings['hint'] ) ) { |
| 567 | ?> |
| 568 | <span class="extra-text"> |
| 569 | <?php echo self::$settings['hint']; ?> |
| 570 | </span> |
| 571 | <?php |
| 572 | } |
| 573 | } |
| 574 | |
| 575 | /** |
| 576 | * Button |
| 577 | * |
| 578 | * @since 4.0.0 |
| 579 | */ |
| 580 | private static function button() { |
| 581 | $type_attr = 'type="button"'; |
| 582 | $pattern = ''; |
| 583 | $step = ''; |
| 584 | $max_chars = ''; |
| 585 | if ( ! empty( self::$edit_type ) ) { |
| 586 | $type_attr = ' type="' . self::$edit_type . '"'; |
| 587 | } |
| 588 | if ( ! empty( self::$validate_pattern ) ) { |
| 589 | $pattern = ' pattern="' . self::$validate_pattern . '"'; |
| 590 | } |
| 591 | if ( ! empty( self::$step ) ) { |
| 592 | $step = ' step="' . self::$step . '"'; |
| 593 | } |
| 594 | if ( ! empty( self::$max_chars ) ) { |
| 595 | $max_chars = ' maxlength="' . self::$max_chars . '"'; |
| 596 | } |
| 597 | |
| 598 | if ( self::$data_attrs && is_array( self::$data_attrs ) && ! empty( self::$data_attrs ) ) { |
| 599 | foreach ( self::$data_attrs as $data_attr_key => $data_attr_value ) { |
| 600 | $type_attr .= ' data-' . $data_attr_key . '="' . \esc_attr( $data_attr_value ) . '"'; |
| 601 | } |
| 602 | } |
| 603 | ?> |
| 604 | <input <?php echo self::$item_id_attr; ?> class="<?php echo self::$custom_class; ?>" <?php echo self::$name_attr; ?> <?php echo $type_attr; ?> value="<?php echo \esc_attr( self::$current_value ); ?>" <?php echo self::$placeholder_attr; ?><?php echo $pattern; ?><?php echo $max_chars; ?><?php echo ( ( self::$required ) ? ' required' : '' ); ?><?php echo $step; ?>> |
| 605 | <?php |
| 606 | } |
| 607 | |
| 608 | /** |
| 609 | * Radio |
| 610 | * |
| 611 | * @since 4.0.0 |
| 612 | */ |
| 613 | private static function radio() { |
| 614 | |
| 615 | ?> |
| 616 | <div class="settings-control <?php echo self::$custom_class; ?>" <?php echo self::$item_id_wrap; ?>> |
| 617 | <?php |
| 618 | $i = 0; |
| 619 | foreach ( self::$settings['options'] as $option_key => $option ) { |
| 620 | ++$i; |
| 621 | |
| 622 | $checked = ''; |
| 623 | if ( ( ! empty( self::$current_value ) && self::$current_value === $option_key ) || ( empty( self::$current_value ) && 1 === $i ) ) { |
| 624 | $checked = 'checked="checked"'; |
| 625 | } |
| 626 | |
| 627 | ?> |
| 628 | <label class="radio-option"> |
| 629 | <input <?php echo self::$name_attr; ?> <?php echo $checked; ?> type="radio" value="<?php echo $option_key; ?>"> |
| 630 | <?php echo $option; ?> |
| 631 | </label> |
| 632 | <?php |
| 633 | } |
| 634 | |
| 635 | ?> |
| 636 | </div> |
| 637 | <div class="clear"></div> |
| 638 | |
| 639 | <?php |
| 640 | if ( empty( self::$settings['toggle'] ) ) { |
| 641 | return; |
| 642 | } |
| 643 | ?> |
| 644 | |
| 645 | <script> |
| 646 | document.addEventListener('DOMContentLoaded', function() { |
| 647 | // Hide all option elements |
| 648 | if ( document.querySelectorAll('.<?php echo esc_js( self::$item_id ); ?>-options') ) { |
| 649 | document.querySelectorAll('.<?php echo esc_js( self::$item_id ); ?>-options').forEach(function(el) { |
| 650 | el.style.display = 'none'; |
| 651 | }); |
| 652 | } |
| 653 | |
| 654 | <?php |
| 655 | if ( isset( self::$settings['toggle'][ self::$current_value ] ) ) { |
| 656 | if ( ! empty( self::$settings['toggle'][ self::$current_value ] ) ) { |
| 657 | ?> |
| 658 | document.querySelector('<?php echo esc_js( self::$settings['toggle'][ self::$current_value ] ); ?>').style.display = 'block'; |
| 659 | <?php |
| 660 | } |
| 661 | } elseif ( is_array( self::$settings['toggle'] ) ) { |
| 662 | $first_elem = reset( self::$settings['toggle'] ); |
| 663 | ?> |
| 664 | if ( '<?php echo esc_js( $first_elem ); ?>'.trim() && document.querySelector('<?php echo esc_js( $first_elem ); ?>') ) { |
| 665 | document.querySelector('<?php echo esc_js( $first_elem ); ?>').style.display = 'block'; |
| 666 | } |
| 667 | <?php |
| 668 | } |
| 669 | ?> |
| 670 | |
| 671 | document.querySelectorAll("input[name='<?php echo esc_js( self::$option_name ); ?>']").forEach(function(radio) { |
| 672 | radio.addEventListener('change', function() { |
| 673 | var selected_val = this.value; |
| 674 | if ( document.querySelectorAll('.<?php echo esc_js( self::$item_id ); ?>-options') ) { |
| 675 | document.querySelectorAll('.<?php echo esc_js( self::$item_id ); ?>-options').forEach(function(el) { |
| 676 | el.style.display = 'none'; |
| 677 | }); |
| 678 | } |
| 679 | |
| 680 | <?php |
| 681 | foreach ( self::$settings['toggle'] as $tg_item_name => $tg_item_id ) { |
| 682 | if ( ! empty( $tg_item_id ) ) { |
| 683 | ?> |
| 684 | if (selected_val === '<?php echo esc_js( $tg_item_name ); ?>') { |
| 685 | document.querySelector('<?php echo esc_js( $tg_item_id ); ?>').style.display = 'block'; |
| 686 | } else { |
| 687 | document.querySelector('<?php echo esc_js( $tg_item_id ); ?>').style.display = 'none'; |
| 688 | } |
| 689 | <?php |
| 690 | } |
| 691 | } |
| 692 | ?> |
| 693 | }); |
| 694 | }); |
| 695 | }); |
| 696 | </script> |
| 697 | <?php |
| 698 | } |
| 699 | |
| 700 | /** |
| 701 | * Section Title |
| 702 | * |
| 703 | * @since 4.0.0 |
| 704 | */ |
| 705 | private static function section_sub_title() { |
| 706 | ?> |
| 707 | <h4 class="section-sub-title <?php echo self::$custom_class; ?>" <?php echo self::$item_id_attr; ?>><?php echo \esc_html( self::$settings['title'] ); ?></h4> |
| 708 | <?php |
| 709 | } |
| 710 | |
| 711 | /** |
| 712 | * Section Title |
| 713 | * |
| 714 | * @since 4.0.0 |
| 715 | */ |
| 716 | private static function section_title() { |
| 717 | ?> |
| 718 | <h3 class="section-title <?php echo self::$custom_class; ?>" <?php echo self::$item_id_attr; ?>><?php echo \esc_html( self::$settings['title'] ); ?></h3> |
| 719 | <?php |
| 720 | } |
| 721 | |
| 722 | /** |
| 723 | * Settings Label |
| 724 | * |
| 725 | * @since 4.0.0 |
| 726 | */ |
| 727 | private static function settings_label() { |
| 728 | ?> |
| 729 | <div class="settings-label"><?php echo self::$settings['text']; ?></div> |
| 730 | <?php |
| 731 | } |
| 732 | |
| 733 | /** |
| 734 | * Checkbox |
| 735 | * |
| 736 | * @since 4.0.0 |
| 737 | */ |
| 738 | private static function checkbox() { |
| 739 | |
| 740 | /** |
| 741 | * Old logic is messy |
| 742 | * The 'not_bool' setting is used when the value of the checkbox is not a boolean, but a specific value (e.g. 'yes' or 'enabled'), and the checkbox should be checked if the current value matches that specific value. In this case, the 'value' setting specifies the value that should be submitted when the checkbox is checked. |
| 743 | */ |
| 744 | if ( self::$not_bool ) { |
| 745 | /** |
| 746 | * For what is worse - some of them have values == element ids, but someones do not. |
| 747 | * This solves the case - once not_bool is set, if the 'not_id' is present, it decides the state comparing it to the value, if not - to the element ID. |
| 748 | */ |
| 749 | if ( isset( self::$settings['not_id'] ) && self::$settings['not_id'] ) { |
| 750 | $checked = \checked( self::$current_value, ( self::$settings['value'] ) ? self::$settings['value'] : '', false ); |
| 751 | } else { |
| 752 | $checked = \checked( self::$current_value, self::$item_id, false ); |
| 753 | } |
| 754 | |
| 755 | $value = self::$settings['value'] ?? ''; |
| 756 | |
| 757 | if ( $value ) { |
| 758 | self::$name_attr .= ' value="' . $value . '"'; |
| 759 | } |
| 760 | } else { |
| 761 | |
| 762 | $checked = \checked( Settings_Utils::string_to_bool( self::$current_value ), true, false ); |
| 763 | } |
| 764 | ?> |
| 765 | |
| 766 | <?php $disabled_attr = ! empty( self::$settings['disabled'] ) ? ' disabled' : ''; ?> |
| 767 | <label class="checkbox-option <?php echo self::$custom_class; ?><?php echo $disabled_attr; ?>"><input type="checkbox" <?php echo self::$item_id_attr; ?> <?php echo self::$name_attr; ?> <?php echo $checked; ?>><?php echo self::$settings['text']; ?></label> |
| 768 | |
| 769 | <?php |
| 770 | } |
| 771 | |
| 772 | /** |
| 773 | * Simple Text |
| 774 | * |
| 775 | * @since 4.0.0 |
| 776 | */ |
| 777 | private static function simple_text() { |
| 778 | ?> |
| 779 | <span <?php echo self::$item_id_wrap; ?> class="<?php echo self::$custom_class; ?>"> |
| 780 | <?php echo self::$settings['text']; ?> |
| 781 | </span> |
| 782 | <?php |
| 783 | } |
| 784 | |
| 785 | /** |
| 786 | * Text |
| 787 | * |
| 788 | * @since 4.0.0 |
| 789 | */ |
| 790 | private static function text() { |
| 791 | $type_attr = 'type="text"'; |
| 792 | $pattern = ''; |
| 793 | $step = ''; |
| 794 | $max_chars = ''; |
| 795 | $title_attr = ''; |
| 796 | |
| 797 | if ( ! empty( self::$edit_type ) ) { |
| 798 | $type_attr = ' type="' . self::$edit_type . '"'; |
| 799 | } |
| 800 | if ( ! empty( self::$title_attr ) ) { |
| 801 | $title_attr = ' title="' . self::$title_attr . '"'; |
| 802 | } |
| 803 | if ( ! empty( self::$validate_pattern ) ) { |
| 804 | $pattern = ' pattern="' . self::$validate_pattern . '"'; |
| 805 | } |
| 806 | if ( ! empty( self::$step ) ) { |
| 807 | $step = ' step="' . self::$step . '"'; |
| 808 | } |
| 809 | if ( ! empty( self::$max_chars ) ) { |
| 810 | $max_chars = ' maxlength="' . self::$max_chars . '"'; |
| 811 | } |
| 812 | |
| 813 | if ( 'password' === self::$edit_type ) { |
| 814 | $type_attr .= ' autocomplete="new-password"'; |
| 815 | } |
| 816 | |
| 817 | if ( self::$data_attrs && is_array( self::$data_attrs ) && ! empty( self::$data_attrs ) ) { |
| 818 | foreach ( self::$data_attrs as $data_attr_key => $data_attr_value ) { |
| 819 | $type_attr .= ' data-' . $data_attr_key . '="' . \esc_attr( $data_attr_value ) . '"'; |
| 820 | } |
| 821 | } |
| 822 | ?> |
| 823 | <input <?php echo self::$item_id_attr; ?> class="<?php echo self::$custom_class; ?>" <?php echo $title_attr; ?> <?php echo self::$name_attr; ?> <?php echo $type_attr; ?> value="<?php echo \esc_attr( self::$current_value ); ?>" <?php echo self::$placeholder_attr; ?><?php echo $pattern; ?><?php echo $max_chars; ?><?php echo ( ( self::$required ) ? ' required' : '' ); ?><?php echo $step; ?>> |
| 824 | <?php |
| 825 | } |
| 826 | |
| 827 | /** |
| 828 | * Secret - masked text input with eye toggle |
| 829 | * |
| 830 | * @since 3.2.1 |
| 831 | */ |
| 832 | private static function secret() { |
| 833 | $pattern = ''; |
| 834 | $step = ''; |
| 835 | $max_chars = ''; |
| 836 | |
| 837 | if ( ! empty( self::$validate_pattern ) ) { |
| 838 | $pattern = ' pattern="' . self::$validate_pattern . '"'; |
| 839 | } |
| 840 | if ( ! empty( self::$step ) ) { |
| 841 | $step = ' step="' . self::$step . '"'; |
| 842 | } |
| 843 | if ( ! empty( self::$max_chars ) ) { |
| 844 | $max_chars = ' maxlength="' . self::$max_chars . '"'; |
| 845 | } |
| 846 | |
| 847 | $data_attr_string = ''; |
| 848 | if ( self::$data_attrs && is_array( self::$data_attrs ) && ! empty( self::$data_attrs ) ) { |
| 849 | foreach ( self::$data_attrs as $data_attr_key => $data_attr_value ) { |
| 850 | $data_attr_string .= ' data-' . $data_attr_key . '="' . \esc_attr( $data_attr_value ) . '"'; |
| 851 | } |
| 852 | } |
| 853 | ?> |
| 854 | <span class="wp2fa-secret-field-wrap"> |
| 855 | <input <?php echo self::$item_id_attr; ?> class="<?php echo self::$custom_class; ?> wp2fa-secret-field" <?php echo self::$name_attr; ?> type="text" autocomplete="off" value="<?php echo \esc_attr( self::$current_value ); ?>" <?php echo self::$placeholder_attr; ?><?php echo $pattern; ?><?php echo $max_chars; ?><?php echo ( ( self::$required ) ? ' required' : '' ); ?><?php echo $step; ?><?php echo $data_attr_string; ?>> |
| 856 | <button type="button" class="wp2fa-secret-toggle" aria-label="<?php \esc_attr_e( 'Show', 'wp-2fa' ); ?>"> |
| 857 | <svg viewBox="0 0 24 24"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zm0 12.5c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg> |
| 858 | </button> |
| 859 | </span> |
| 860 | <?php |
| 861 | } |
| 862 | |
| 863 | /** |
| 864 | * Description Message |
| 865 | * |
| 866 | * @since 4.0.0 |
| 867 | */ |
| 868 | private static function description() { |
| 869 | ?> |
| 870 | <p <?php echo self::$item_id_wrap; ?> class="description <?php echo self::$custom_class; ?>"> |
| 871 | <?php echo self::$settings['text']; ?> |
| 872 | </p> |
| 873 | <?php |
| 874 | } |
| 875 | |
| 876 | /** |
| 877 | * Color Picker - uses WP core wp-color-picker |
| 878 | * |
| 879 | * @since 4.0.0 |
| 880 | */ |
| 881 | private static function color() { |
| 882 | ?> |
| 883 | <div class="wp2fa-color-picker-wrap"> |
| 884 | <input <?php echo self::$item_id_attr; ?> <?php echo self::$name_attr; ?> |
| 885 | type="text" |
| 886 | class="wp2fa-color-picker<?php echo self::$custom_class; ?>" |
| 887 | value="<?php echo \esc_attr( self::$current_value ); ?>"> |
| 888 | </div> |
| 889 | <?php |
| 890 | } |
| 891 | |
| 892 | /** |
| 893 | * Image / Logo selector - uses WP core media library |
| 894 | * |
| 895 | * Expected settings keys: |
| 896 | * 'button_text' (string) - Label on the select button. |
| 897 | * 'description' (string) - Helper text shown next to the button. |
| 898 | * |
| 899 | * @since 4.0.0 |
| 900 | */ |
| 901 | private static function image_select() { |
| 902 | $button_text = ! empty( self::$settings['button_text'] ) ? self::$settings['button_text'] : \esc_html__( 'Select image', 'wp-2fa' ); |
| 903 | $description = ! empty( self::$settings['description'] ) ? self::$settings['description'] : ''; |
| 904 | $has_image = ! empty( self::$current_value ); |
| 905 | ?> |
| 906 | <div class="wp2fa-image-select-wrap"> |
| 907 | <div class="wp2fa-image-select-controls"> |
| 908 | <button type="button" |
| 909 | class="button wp2fa-media-upload<?php echo self::$custom_class; ?>" |
| 910 | data-target="<?php echo \esc_attr( self::$item_id ); ?>"> |
| 911 | <?php echo \esc_html( $button_text ); ?> |
| 912 | </button> |
| 913 | <?php if ( $description ) : ?> |
| 914 | <span class="wp2fa-image-desc"><?php echo \esc_html( $description ); ?></span> |
| 915 | <?php endif; ?> |
| 916 | </div> |
| 917 | <input <?php echo self::$item_id_attr; ?> <?php echo self::$name_attr; ?> |
| 918 | type="hidden" |
| 919 | value="<?php echo \esc_attr( self::$current_value ); ?>"> |
| 920 | <div class="wp2fa-image-preview<?php echo $has_image ? '' : ' hidden'; ?>"> |
| 921 | <img src="<?php echo $has_image ? \esc_url( self::$current_value ) : ''; ?>" alt=""> |
| 922 | <button type="button" class="wp2fa-media-remove" |
| 923 | title="<?php \esc_attr_e( 'Remove', 'wp-2fa' ); ?>">✕</button> |
| 924 | </div> |
| 925 | </div> |
| 926 | <?php |
| 927 | } |
| 928 | |
| 929 | /** |
| 930 | * Font selector - a styled <select> with common web-safe font stacks. |
| 931 | * The stored value is the full CSS font-family string (e.g. "Arial, sans-serif"). |
| 932 | * |
| 933 | * @since 4.0.0 |
| 934 | */ |
| 935 | private static function font_select() { |
| 936 | $font_families = array( |
| 937 | 'Arial, Helvetica, sans-serif' => 'Arial', |
| 938 | "'Arial Black', Gadget, sans-serif" => 'Arial Black', |
| 939 | "'Bookman Old Style', serif" => 'Bookman Old Style', |
| 940 | "'Comic Sans MS', cursive" => 'Comic Sans MS', |
| 941 | 'Courier, monospace' => 'Courier', |
| 942 | 'Garamond, serif' => 'Garamond', |
| 943 | 'Georgia, serif' => 'Georgia', |
| 944 | 'Impact, Charcoal, sans-serif' => 'Impact', |
| 945 | "'Lucida Console', Monaco, monospace" => 'Lucida Console', |
| 946 | "'Lucida Sans Unicode', 'Lucida Grande', sans-serif" => 'Lucida Sans', |
| 947 | "'MS Sans Serif', Geneva, sans-serif" => 'MS Sans Serif', |
| 948 | "'MS Serif', 'New York', sans-serif" => 'MS Serif', |
| 949 | "'Palatino Linotype', 'Book Antiqua', Palatino, serif" => 'Palatino Linotype', |
| 950 | 'Tahoma, Geneva, sans-serif' => 'Tahoma', |
| 951 | "'Times New Roman', Times, serif" => 'Times New Roman', |
| 952 | "'Trebuchet MS', Helvetica, sans-serif" => 'Trebuchet MS', |
| 953 | 'Verdana, Geneva, sans-serif' => 'Verdana', |
| 954 | ); |
| 955 | ?> |
| 956 | <div class="wp2fa-font-select-wrap"> |
| 957 | <select <?php echo self::$item_id_attr; ?> <?php echo self::$name_attr; ?> |
| 958 | class="wp2fa-font-select<?php echo self::$custom_class; ?>"> |
| 959 | <?php |
| 960 | foreach ( $font_families as $value => $label ) { |
| 961 | $selected = \selected( self::$current_value, $value, false ); |
| 962 | ?> |
| 963 | <option value="<?php echo \esc_attr( $value ); ?>" <?php echo $selected; ?> |
| 964 | style="font-family: <?php echo \esc_attr( $value ); ?>;"> |
| 965 | <?php echo \esc_html( $label ); ?> |
| 966 | </option> |
| 967 | <?php |
| 968 | } |
| 969 | ?> |
| 970 | </select> |
| 971 | </div> |
| 972 | <?php |
| 973 | } |
| 974 | |
| 975 | /** |
| 976 | * Toggle Checkbox - renders a toggle switch that behaves like a checkbox. |
| 977 | * |
| 978 | * @since 4.0.0 |
| 979 | */ |
| 980 | private static function toggle_checkbox() { |
| 981 | |
| 982 | if ( self::$not_bool ) { |
| 983 | if ( isset( self::$settings['not_id'] ) && self::$settings['not_id'] ) { |
| 984 | $checked = \checked( self::$current_value, ( isset( self::$settings['value'] ) ? self::$settings['value'] : '' ), false ); |
| 985 | } else { |
| 986 | $checked = \checked( self::$current_value, self::$item_id, false ); |
| 987 | } |
| 988 | $value = self::$settings['value'] ?? ''; |
| 989 | |
| 990 | if ( $value ) { |
| 991 | self::$name_attr .= ' value="' . $value . '"'; |
| 992 | } |
| 993 | } else { |
| 994 | $checked = \checked( Settings_Utils::string_to_bool( self::$current_value ), true, false ); |
| 995 | } |
| 996 | |
| 997 | $extra_attrs = ''; |
| 998 | if ( self::$data_attrs && is_array( self::$data_attrs ) && ! empty( self::$data_attrs ) ) { |
| 999 | foreach ( self::$data_attrs as $data_attr_key => $data_attr_value ) { |
| 1000 | $extra_attrs .= ' data-' . \esc_attr( $data_attr_key ) . '="' . \esc_attr( $data_attr_value ) . '"'; |
| 1001 | } |
| 1002 | } |
| 1003 | ?> |
| 1004 | <label class="toggle-switch <?php echo self::$custom_class; ?>"> |
| 1005 | <input type="checkbox" <?php echo self::$item_id_attr; ?> <?php echo self::$name_attr; ?> <?php echo $checked; ?><?php echo $extra_attrs; ?>> |
| 1006 | <span class="toggle-slider"></span> |
| 1007 | <?php if ( ! empty( self::$settings['text'] ) ) { ?> |
| 1008 | <span class="toggle-label"><?php echo self::$settings['text']; ?></span> |
| 1009 | <?php } ?> |
| 1010 | </label> |
| 1011 | <?php |
| 1012 | } |
| 1013 | |
| 1014 | /** |
| 1015 | * User Roles - renders a multi-column grid of checkboxes for user roles. |
| 1016 | * |
| 1017 | * Expected settings keys: |
| 1018 | * 'roles' (array) - Associative array of role_slug => display_name. |
| 1019 | * 'checked' (array) - Array of role slugs that are currently checked. |
| 1020 | * 'name_prefix' (string) - The name attribute prefix for each checkbox. |
| 1021 | * |
| 1022 | * @since 4.0.0 |
| 1023 | */ |
| 1024 | private static function user_roles() { |
| 1025 | $roles = ! empty( self::$settings['roles'] ) ? self::$settings['roles'] : array(); |
| 1026 | $checked = ! empty( self::$settings['checked'] ) ? self::$settings['checked'] : array(); |
| 1027 | $name_prefix = ! empty( self::$settings['name_prefix'] ) ? self::$settings['name_prefix'] : ''; |
| 1028 | ?> |
| 1029 | <div <?php echo self::$item_id_wrap; ?> class="user-roles-grid <?php echo self::$custom_class; ?>"> |
| 1030 | <?php foreach ( $roles as $role_slug => $role_name ) { ?> |
| 1031 | <label class="checkbox-option user-role-item" for="role-<?php echo \esc_attr( $role_slug ); ?>"> |
| 1032 | <input type="checkbox" |
| 1033 | id="role-<?php echo \esc_attr( $role_slug ); ?>" |
| 1034 | name="<?php echo \esc_attr( $name_prefix ); ?>[<?php echo \esc_attr( $role_slug ); ?>]" |
| 1035 | value="yes" |
| 1036 | <?php \checked( in_array( $role_slug, $checked, true ) ); ?>> |
| 1037 | <?php echo \esc_html( $role_name ); ?> |
| 1038 | </label> |
| 1039 | <?php } ?> |
| 1040 | </div> |
| 1041 | <?php |
| 1042 | } |
| 1043 | |
| 1044 | /** |
| 1045 | * Stat Cards - renders a row of statistic summary cards. |
| 1046 | * |
| 1047 | * Expected settings keys: |
| 1048 | * 'cards' (array) - Array of arrays with 'value' and 'label' keys. |
| 1049 | * |
| 1050 | * @since 4.0.0 |
| 1051 | */ |
| 1052 | private static function stat_cards() { |
| 1053 | $cards = ! empty( self::$settings['cards'] ) ? self::$settings['cards'] : array(); |
| 1054 | ?> |
| 1055 | <div <?php echo self::$item_id_wrap; ?> class="stat-cards-row <?php echo self::$custom_class; ?>"> |
| 1056 | <?php foreach ( $cards as $card ) { ?> |
| 1057 | <div class="stat-card"> |
| 1058 | <span class="stat-value"><?php echo \esc_html( (string) $card['value'] ); ?></span> |
| 1059 | <span class="stat-label"><?php echo \esc_html( $card['label'] ); ?></span> |
| 1060 | </div> |
| 1061 | <?php } ?> |
| 1062 | </div> |
| 1063 | <?php |
| 1064 | } |
| 1065 | |
| 1066 | /** |
| 1067 | * Data Table - renders a data table with header and rows. |
| 1068 | * |
| 1069 | * Expected settings keys: |
| 1070 | * 'header' (array) - Array of column header strings. |
| 1071 | * 'rows' (array) - Array of row arrays. Each row value can be a string or |
| 1072 | * an associative array with 'url' and 'label' (renders as a link). |
| 1073 | * 'table_id' (string) - Optional HTML id for the table element. |
| 1074 | * |
| 1075 | * @since 4.0.0 |
| 1076 | */ |
| 1077 | private static function data_table() { |
| 1078 | $header = ! empty( self::$settings['header'] ) ? self::$settings['header'] : array(); |
| 1079 | $rows = ! empty( self::$settings['rows'] ) ? self::$settings['rows'] : array(); |
| 1080 | $table_id = ! empty( self::$settings['table_id'] ) ? self::$settings['table_id'] : ''; |
| 1081 | $id_attr = $table_id ? ' id="' . \esc_attr( $table_id ) . '"' : ''; |
| 1082 | ?> |
| 1083 | <div class="report-table-wrap <?php echo self::$custom_class; ?>"> |
| 1084 | <table class="report-data-table"<?php echo $id_attr; ?>> |
| 1085 | <?php if ( ! empty( $header ) ) { ?> |
| 1086 | <thead> |
| 1087 | <tr> |
| 1088 | <?php foreach ( $header as $cell ) { ?> |
| 1089 | <th><?php echo \esc_html( $cell ); ?></th> |
| 1090 | <?php } ?> |
| 1091 | </tr> |
| 1092 | </thead> |
| 1093 | <?php } ?> |
| 1094 | <tbody> |
| 1095 | <?php foreach ( $rows as $row ) { ?> |
| 1096 | <tr> |
| 1097 | <?php foreach ( $row as $cell ) { ?> |
| 1098 | <td> |
| 1099 | <?php |
| 1100 | if ( \is_array( $cell ) && isset( $cell['url'], $cell['label'] ) ) { |
| 1101 | echo '<a href="' . \esc_url( $cell['url'] ) . '">' . \esc_html( $cell['label'] ) . '</a>'; |
| 1102 | } else { |
| 1103 | echo \esc_html( (string) $cell ); |
| 1104 | } |
| 1105 | ?> |
| 1106 | </td> |
| 1107 | <?php } ?> |
| 1108 | </tr> |
| 1109 | <?php } ?> |
| 1110 | </tbody> |
| 1111 | </table> |
| 1112 | </div> |
| 1113 | <?php |
| 1114 | } |
| 1115 | |
| 1116 | /** |
| 1117 | * Multi-select with AJAX autocomplete search (select2-like, vanilla JS). |
| 1118 | * |
| 1119 | * Expected settings keys: |
| 1120 | * 'items' (array) - Array of [ 'id' => ..., 'text' => ... ] for pre-selected items. |
| 1121 | * 'placeholder' (string) - Placeholder text for the search input. |
| 1122 | * 'data_attrs' (array) - Must contain 'search-type' ('users' or 'roles'). |
| 1123 | * Optional 'min-chars' (default '2'). |
| 1124 | * |
| 1125 | * The hidden input stores a comma-separated list of selected IDs for form submission. |
| 1126 | * |
| 1127 | * @since 4.0.0 |
| 1128 | */ |
| 1129 | private static function multi_select_ajax() { |
| 1130 | $items = ! empty( self::$settings['items'] ) ? self::$settings['items'] : array(); |
| 1131 | $search_type = ''; |
| 1132 | $min_chars = '2'; |
| 1133 | |
| 1134 | if ( ! empty( self::$data_attrs ) && is_array( self::$data_attrs ) ) { |
| 1135 | $search_type = isset( self::$data_attrs['search-type'] ) ? self::$data_attrs['search-type'] : ''; |
| 1136 | $min_chars = isset( self::$data_attrs['min-chars'] ) ? self::$data_attrs['min-chars'] : '2'; |
| 1137 | } |
| 1138 | |
| 1139 | $placeholder = ! empty( self::$settings['placeholder'] ) ? self::$settings['placeholder'] : ''; |
| 1140 | |
| 1141 | if ( ! self::$multi_select_ajax_rendered ) { |
| 1142 | self::multi_select_ajax_assets(); |
| 1143 | self::$multi_select_ajax_rendered = true; |
| 1144 | } |
| 1145 | |
| 1146 | ?> |
| 1147 | <div class="wp2fa-msa-wrap<?php echo self::$custom_class; ?>" |
| 1148 | <?php echo self::$item_id_wrap; ?> |
| 1149 | data-search-type="<?php echo \esc_attr( $search_type ); ?>" |
| 1150 | data-min-chars="<?php echo \esc_attr( $min_chars ); ?>"> |
| 1151 | <div class="wp2fa-msa-tags-input"> |
| 1152 | <?php foreach ( $items as $item ) { ?> |
| 1153 | <span class="wp2fa-msa-tag" data-id="<?php echo \esc_attr( $item['id'] ); ?>"> |
| 1154 | <?php echo \esc_html( $item['text'] ); ?> |
| 1155 | <button type="button" class="wp2fa-msa-remove" aria-label="<?php \esc_attr_e( 'Remove', 'wp-2fa' ); ?>">×</button> |
| 1156 | </span> |
| 1157 | <?php } ?> |
| 1158 | <input type="text" |
| 1159 | class="wp2fa-msa-input" |
| 1160 | placeholder="<?php echo \esc_attr( $placeholder ); ?>" |
| 1161 | autocomplete="off"> |
| 1162 | </div> |
| 1163 | <div class="wp2fa-msa-dropdown"></div> |
| 1164 | <input type="hidden" |
| 1165 | class="wp2fa-msa-value" |
| 1166 | <?php echo self::$name_attr; ?> |
| 1167 | value="<?php echo \esc_attr( self::$current_value ); ?>"> |
| 1168 | </div> |
| 1169 | <?php |
| 1170 | } |
| 1171 | |
| 1172 | /** |
| 1173 | * Output the shared CSS and JS assets for the multi-select-ajax component. |
| 1174 | * Called once per page load via the static flag. |
| 1175 | * |
| 1176 | * @since 4.0.0 |
| 1177 | */ |
| 1178 | private static function multi_select_ajax_assets() { |
| 1179 | ?> |
| 1180 | <style> |
| 1181 | .wp2fa-msa-wrap{position:relative;max-width:500px} |
| 1182 | .wp2fa-msa-tags-input{display:flex;flex-wrap:wrap;align-items:center;gap:4px;padding:4px 8px;border:1px solid #8c8f94;border-radius:4px;background:#fff;cursor:text;min-height:36px} |
| 1183 | .wp2fa-msa-tags-input:focus-within{border-color:#2271b1;box-shadow:0 0 0 1px #2271b1} |
| 1184 | .wp2fa-msa-tag{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;background:#f0f0f1;border:1px solid #c3c4c7;border-radius:3px;font-size:13px;line-height:1.4;white-space:nowrap} |
| 1185 | .wp2fa-msa-remove{background:none;border:none;padding:0 2px;cursor:pointer;font-size:16px;line-height:1;color:#787c82} |
| 1186 | .wp2fa-msa-remove:hover{color:#d63638} |
| 1187 | .wp2fa-msa-input{border:none !important;outline:none !important;box-shadow:none !important;padding:2px 4px !important;min-width:120px;flex:1;font-size:13px;background:transparent !important} |
| 1188 | .wp2fa-msa-dropdown{display:none;position:absolute;top:100%;left:0;right:0;z-index:1000;background:#fff;border:1px solid #8c8f94;border-top:none;border-radius:0 0 4px 4px;max-height:200px;overflow-y:auto;box-shadow:0 2px 4px rgba(0,0,0,.1)} |
| 1189 | .wp2fa-msa-option{padding:8px 12px;cursor:pointer;font-size:13px} |
| 1190 | .wp2fa-msa-option:hover,.wp2fa-msa-option.wp2fa-msa-active{background:#2271b1;color:#fff} |
| 1191 | .wp2fa-msa-no-results{padding:8px 12px;color:#787c82;font-size:13px;font-style:italic} |
| 1192 | </style> |
| 1193 | <script> |
| 1194 | (function(){ |
| 1195 | 'use strict'; |
| 1196 | if(window.wp2faMSAInit)return; |
| 1197 | window.wp2faMSAInit=true; |
| 1198 | |
| 1199 | /* Mapping of opposite selector wrapper IDs. */ |
| 1200 | var oppositePairs={ |
| 1201 | 'enforced-roles-item':'excluded-roles-item', |
| 1202 | 'excluded-roles-item':'enforced-roles-item', |
| 1203 | 'enforced-users-item':'excluded-users-item', |
| 1204 | 'excluded-users-item':'enforced-users-item' |
| 1205 | }; |
| 1206 | |
| 1207 | /** |
| 1208 | * Return the IDs currently selected in the opposite selector, |
| 1209 | * or an empty array if there is no opposite. |
| 1210 | */ |
| 1211 | function getOppositeIds(wrap){ |
| 1212 | var wrapId=wrap.getAttribute('id')||''; |
| 1213 | var oppositeId=oppositePairs[wrapId]; |
| 1214 | if(!oppositeId)return[]; |
| 1215 | var opp=document.getElementById(oppositeId); |
| 1216 | if(!opp)return[]; |
| 1217 | var h=opp.querySelector('.wp2fa-msa-value'); |
| 1218 | if(!h||!h.value.trim())return[]; |
| 1219 | return h.value.trim().split(',').map(function(s){return s.trim()}).filter(Boolean); |
| 1220 | } |
| 1221 | |
| 1222 | /** |
| 1223 | * Remove a single ID from the opposite selector |
| 1224 | * (both the hidden value and the visible tag). |
| 1225 | */ |
| 1226 | function removeFromOpposite(wrap,id){ |
| 1227 | var wrapId=wrap.getAttribute('id')||''; |
| 1228 | var oppositeId=oppositePairs[wrapId]; |
| 1229 | if(!oppositeId)return; |
| 1230 | var opp=document.getElementById(oppositeId); |
| 1231 | if(!opp)return; |
| 1232 | var h=opp.querySelector('.wp2fa-msa-value'); |
| 1233 | if(!h)return; |
| 1234 | var ids=h.value.trim()?h.value.trim().split(',').map(function(s){return s.trim()}).filter(Boolean):[]; |
| 1235 | if(ids.indexOf(id)===-1)return; |
| 1236 | ids=ids.filter(function(i){return i!==id}); |
| 1237 | h.value=ids.join(','); |
| 1238 | var tag=opp.querySelector('.wp2fa-msa-tag[data-id="'+id+'"]'); |
| 1239 | if(tag)tag.parentNode.removeChild(tag); |
| 1240 | } |
| 1241 | |
| 1242 | function initMSA(wrap,cfg){ |
| 1243 | var tagsInput=wrap.querySelector('.wp2fa-msa-tags-input'); |
| 1244 | var input=wrap.querySelector('.wp2fa-msa-input'); |
| 1245 | var dropdown=wrap.querySelector('.wp2fa-msa-dropdown'); |
| 1246 | var hiddenInput=wrap.querySelector('.wp2fa-msa-value'); |
| 1247 | var searchType=wrap.getAttribute('data-search-type')||''; |
| 1248 | var minChars=parseInt(wrap.getAttribute('data-min-chars')||'2',10); |
| 1249 | var debounceTimer=null; |
| 1250 | var currentXhr=null; |
| 1251 | var activeIndex=-1; |
| 1252 | |
| 1253 | function getIds(){ |
| 1254 | var v=hiddenInput.value.trim(); |
| 1255 | if(!v)return[]; |
| 1256 | return v.split(',').map(function(s){return s.trim()}).filter(Boolean); |
| 1257 | } |
| 1258 | |
| 1259 | function setIds(ids){ |
| 1260 | hiddenInput.value=ids.join(','); |
| 1261 | } |
| 1262 | |
| 1263 | function addTag(id,text){ |
| 1264 | var ids=getIds(); |
| 1265 | if(ids.indexOf(id)!==-1)return; |
| 1266 | |
| 1267 | /* Remove from the opposite selector first. */ |
| 1268 | removeFromOpposite(wrap,id); |
| 1269 | |
| 1270 | ids.push(id); |
| 1271 | setIds(ids); |
| 1272 | |
| 1273 | var tag=document.createElement('span'); |
| 1274 | tag.className='wp2fa-msa-tag'; |
| 1275 | tag.setAttribute('data-id',id); |
| 1276 | |
| 1277 | var label=document.createTextNode(text+' '); |
| 1278 | tag.appendChild(label); |
| 1279 | |
| 1280 | var btn=document.createElement('button'); |
| 1281 | btn.type='button'; |
| 1282 | btn.className='wp2fa-msa-remove'; |
| 1283 | btn.setAttribute('aria-label','Remove'); |
| 1284 | btn.innerHTML='×'; |
| 1285 | btn.addEventListener('click',function(e){ |
| 1286 | e.stopPropagation(); |
| 1287 | removeTag(id); |
| 1288 | }); |
| 1289 | tag.appendChild(btn); |
| 1290 | |
| 1291 | tagsInput.insertBefore(tag,input); |
| 1292 | } |
| 1293 | |
| 1294 | function removeTag(id){ |
| 1295 | var ids=getIds().filter(function(i){return i!==id}); |
| 1296 | setIds(ids); |
| 1297 | var tag=wrap.querySelector('.wp2fa-msa-tag[data-id="'+id+'"]'); |
| 1298 | if(tag)tag.parentNode.removeChild(tag); |
| 1299 | } |
| 1300 | |
| 1301 | function hideDropdown(){ |
| 1302 | dropdown.style.display='none'; |
| 1303 | dropdown.innerHTML=''; |
| 1304 | activeIndex=-1; |
| 1305 | } |
| 1306 | |
| 1307 | function showDropdown(results){ |
| 1308 | dropdown.innerHTML=''; |
| 1309 | activeIndex=-1; |
| 1310 | var ids=getIds(); |
| 1311 | var oppIds=getOppositeIds(wrap); |
| 1312 | var filtered=results.filter(function(r){return ids.indexOf(r.id)===-1&&oppIds.indexOf(r.id)===-1}); |
| 1313 | |
| 1314 | if(!filtered.length){ |
| 1315 | var noRes=document.createElement('div'); |
| 1316 | noRes.className='wp2fa-msa-no-results'; |
| 1317 | noRes.textContent='No results found'; |
| 1318 | dropdown.appendChild(noRes); |
| 1319 | dropdown.style.display='block'; |
| 1320 | return; |
| 1321 | } |
| 1322 | |
| 1323 | filtered.forEach(function(item){ |
| 1324 | var opt=document.createElement('div'); |
| 1325 | opt.className='wp2fa-msa-option'; |
| 1326 | opt.textContent=item.text; |
| 1327 | opt.setAttribute('data-id',item.id); |
| 1328 | opt.addEventListener('mousedown',function(e){ |
| 1329 | e.preventDefault(); |
| 1330 | addTag(item.id,item.text); |
| 1331 | input.value=''; |
| 1332 | hideDropdown(); |
| 1333 | }); |
| 1334 | dropdown.appendChild(opt); |
| 1335 | }); |
| 1336 | dropdown.style.display='block'; |
| 1337 | } |
| 1338 | |
| 1339 | function setActive(idx){ |
| 1340 | var opts=dropdown.querySelectorAll('.wp2fa-msa-option'); |
| 1341 | for(var i=0;i<opts.length;i++){opts[i].classList.remove('wp2fa-msa-active')} |
| 1342 | if(idx>=0&&idx<opts.length){ |
| 1343 | opts[idx].classList.add('wp2fa-msa-active'); |
| 1344 | opts[idx].scrollIntoView({block:'nearest'}); |
| 1345 | } |
| 1346 | activeIndex=idx; |
| 1347 | } |
| 1348 | |
| 1349 | function doSearch(term){ |
| 1350 | if(currentXhr){currentXhr.abort();currentXhr=null} |
| 1351 | |
| 1352 | var xhr=new XMLHttpRequest(); |
| 1353 | currentXhr=xhr; |
| 1354 | |
| 1355 | var url=cfg.ajaxUrl |
| 1356 | +'?action='+encodeURIComponent(cfg.searchAction) |
| 1357 | +'&nonce='+encodeURIComponent(cfg.nonce) |
| 1358 | +'&type='+encodeURIComponent(searchType) |
| 1359 | +'&term='+encodeURIComponent(term); |
| 1360 | |
| 1361 | xhr.open('GET',url,true); |
| 1362 | xhr.onreadystatechange=function(){ |
| 1363 | if(xhr.readyState!==4)return; |
| 1364 | currentXhr=null; |
| 1365 | if(xhr.status>=200&&xhr.status<300){ |
| 1366 | try{ |
| 1367 | var resp=JSON.parse(xhr.responseText); |
| 1368 | if(resp.success&&Array.isArray(resp.data)){ |
| 1369 | showDropdown(resp.data); |
| 1370 | } |
| 1371 | }catch(e){hideDropdown()} |
| 1372 | } |
| 1373 | }; |
| 1374 | xhr.send(); |
| 1375 | } |
| 1376 | |
| 1377 | input.addEventListener('input',function(){ |
| 1378 | var term=input.value.trim(); |
| 1379 | if(debounceTimer)clearTimeout(debounceTimer); |
| 1380 | if(term.length<minChars){hideDropdown();return} |
| 1381 | debounceTimer=setTimeout(function(){doSearch(term)},250); |
| 1382 | }); |
| 1383 | |
| 1384 | input.addEventListener('keydown',function(e){ |
| 1385 | var opts=dropdown.querySelectorAll('.wp2fa-msa-option'); |
| 1386 | if(e.key==='ArrowDown'){ |
| 1387 | e.preventDefault(); |
| 1388 | if(activeIndex<opts.length-1)setActive(activeIndex+1); |
| 1389 | }else if(e.key==='ArrowUp'){ |
| 1390 | e.preventDefault(); |
| 1391 | if(activeIndex>0)setActive(activeIndex-1); |
| 1392 | }else if(e.key==='Enter'){ |
| 1393 | e.preventDefault(); |
| 1394 | if(activeIndex>=0&&opts[activeIndex]){ |
| 1395 | var id=opts[activeIndex].getAttribute('data-id'); |
| 1396 | var text=opts[activeIndex].textContent; |
| 1397 | addTag(id,text); |
| 1398 | input.value=''; |
| 1399 | hideDropdown(); |
| 1400 | } |
| 1401 | }else if(e.key==='Escape'){ |
| 1402 | hideDropdown(); |
| 1403 | }else if(e.key==='Backspace'&&!input.value){ |
| 1404 | var tags=wrap.querySelectorAll('.wp2fa-msa-tag'); |
| 1405 | if(tags.length){ |
| 1406 | var last=tags[tags.length-1]; |
| 1407 | removeTag(last.getAttribute('data-id')); |
| 1408 | } |
| 1409 | } |
| 1410 | }); |
| 1411 | |
| 1412 | input.addEventListener('blur',function(){ |
| 1413 | setTimeout(hideDropdown,200); |
| 1414 | }); |
| 1415 | |
| 1416 | tagsInput.addEventListener('click',function(e){ |
| 1417 | if(e.target===input)return; |
| 1418 | input.focus(); |
| 1419 | }); |
| 1420 | |
| 1421 | /* Attach remove handlers for server-rendered tags. */ |
| 1422 | var existingBtns=wrap.querySelectorAll('.wp2fa-msa-remove'); |
| 1423 | for(var i=0;i<existingBtns.length;i++){ |
| 1424 | (function(btn){ |
| 1425 | btn.addEventListener('click',function(e){ |
| 1426 | e.stopPropagation(); |
| 1427 | var tag=btn.parentNode; |
| 1428 | if(tag&&tag.classList.contains('wp2fa-msa-tag')){ |
| 1429 | removeTag(tag.getAttribute('data-id')); |
| 1430 | } |
| 1431 | }); |
| 1432 | })(existingBtns[i]); |
| 1433 | } |
| 1434 | } |
| 1435 | |
| 1436 | function boot(){ |
| 1437 | var cfg=window.wp2faSavePoliciesNew; |
| 1438 | if(!cfg||!cfg.searchAction)return; |
| 1439 | var wraps=document.querySelectorAll('.wp2fa-msa-wrap'); |
| 1440 | for(var i=0;i<wraps.length;i++){initMSA(wraps[i],cfg)} |
| 1441 | } |
| 1442 | |
| 1443 | if(document.readyState==='loading'){ |
| 1444 | document.addEventListener('DOMContentLoaded',boot); |
| 1445 | }else{ |
| 1446 | boot(); |
| 1447 | } |
| 1448 | })(); |
| 1449 | </script> |
| 1450 | <?php |
| 1451 | } |
| 1452 | |
| 1453 | /** |
| 1454 | * Tab Title |
| 1455 | * |
| 1456 | * @since 4.0.0 |
| 1457 | */ |
| 1458 | private static function tab_title() { |
| 1459 | ?> |
| 1460 | <h2> |
| 1461 | <?php |
| 1462 | |
| 1463 | echo \esc_html( self::$settings['title'] ); |
| 1464 | ?> |
| 1465 | </h2> |
| 1466 | <?php |
| 1467 | } |
| 1468 | |
| 1469 | /** |
| 1470 | * Group Title Builder |
| 1471 | * |
| 1472 | * @return void |
| 1473 | * |
| 1474 | * @since 4.0.0 |
| 1475 | */ |
| 1476 | private static function group_title() { |
| 1477 | ?> |
| 1478 | <div <?php echo self::$item_id_attr; ?> class="group-title <?php echo self::$custom_class; ?>"><?php echo \esc_html( self::$current_value ); ?></div> |
| 1479 | <?php |
| 1480 | } |
| 1481 | |
| 1482 | /** |
| 1483 | * List Builder |
| 1484 | * |
| 1485 | * @return void |
| 1486 | * |
| 1487 | * @since 4.0.0 |
| 1488 | */ |
| 1489 | private static function list() { |
| 1490 | echo '<ul ' . self::$item_id_wrap . ' class="wp-2fa-list-options' . self::$custom_class . '">'; |
| 1491 | |
| 1492 | foreach ( self::$current_value as $value ) { |
| 1493 | ?> |
| 1494 | <li tabindex="0"> |
| 1495 | <span class="option-value" id="<?php echo \esc_attr( $value['id'] ); ?>"><?php echo \esc_html( $value['title'] ); ?></span> |
| 1496 | <?php if ( ! empty( self::$settings['values'][ $value['id'] ]['badge'] ) ) : ?> |
| 1497 | <span class="wp-2fa-badge"><?php echo \esc_html( self::$settings['values'][ $value['id'] ]['badge'] ); ?></span> |
| 1498 | <?php endif; ?> |
| 1499 | </li> |
| 1500 | <?php |
| 1501 | } |
| 1502 | |
| 1503 | echo '</ul>'; |
| 1504 | } |
| 1505 | |
| 1506 | /** |
| 1507 | * Number |
| 1508 | * |
| 1509 | * @since 5.0.0 |
| 1510 | */ |
| 1511 | private static function number() { |
| 1512 | |
| 1513 | $min = ! empty( self::$min ) ? self::$min : -1000; |
| 1514 | $max = ! empty( self::$max ) ? self::$max : 1000000; |
| 1515 | |
| 1516 | ?> |
| 1517 | <div class="number-input-wrap"> |
| 1518 | <input style="width:100px" min="<?php echo $min; ?>" max="<?php echo $max; ?>" <?php echo self::$item_id_attr; ?> <?php echo self::$name_attr; ?> type="number" value="<?php echo \esc_attr( self::$current_value ); ?>" <?php echo self::$placeholder_attr; ?>> |
| 1519 | <?php |
| 1520 | if ( self::$settings['label'] ?? false ) { |
| 1521 | echo '<span class="number-label">' . \esc_html( self::$settings['label'] ) . '</span>'; |
| 1522 | } |
| 1523 | ?> |
| 1524 | </div> |
| 1525 | <?php |
| 1526 | } |
| 1527 | |
| 1528 | /** |
| 1529 | * Select dropdown. |
| 1530 | * |
| 1531 | * Expected settings keys: |
| 1532 | * 'options' (array) - Associative array of value => label. |
| 1533 | * |
| 1534 | * @since 5.0.0 |
| 1535 | */ |
| 1536 | private static function select() { |
| 1537 | ?> |
| 1538 | <select <?php echo self::$item_id_attr; ?> <?php echo self::$name_attr; ?> class="<?php echo self::$custom_class; ?>"> |
| 1539 | <?php |
| 1540 | if ( ! empty( self::$settings['options'] ) && \is_array( self::$settings['options'] ) ) { |
| 1541 | foreach ( self::$settings['options'] as $option_key => $option ) { |
| 1542 | $selected = \selected( self::$current_value, $option_key, false ); |
| 1543 | ?> |
| 1544 | <option value="<?php echo \esc_attr( $option_key ); ?>" <?php echo $selected; ?>><?php echo \esc_html( $option ); ?></option> |
| 1545 | <?php |
| 1546 | } |
| 1547 | } |
| 1548 | ?> |
| 1549 | </select> |
| 1550 | <?php |
| 1551 | } |
| 1552 | |
| 1553 | /** |
| 1554 | * Renders a breadcrumb navigation bar. |
| 1555 | * |
| 1556 | * @param string $parent_label - The escaped parent page label. |
| 1557 | * @param string $parent_back_class - The CSS class used for the back navigation. |
| 1558 | * @param string $current_label - The escaped current page label. |
| 1559 | * |
| 1560 | * @return void |
| 1561 | * |
| 1562 | * @since 4.0.0 |
| 1563 | */ |
| 1564 | public static function breadcrumb() { |
| 1565 | ?> |
| 1566 | <nav class="breadcrumbs"> |
| 1567 | <a href="#" class="back-button back-click" aria-label="<?php echo \esc_attr__( 'Go back', 'wp-2fa' ); ?>"><svg xmlns="http://www.w3.org/2000/svg" width="7" height="12" viewBox="0 0 7 12" fill="none" aria-hidden="true"><path d="M5.9375 0.9375L0.9375 5.9375L5.9375 10.9375" stroke="white" stroke-width="1.875" stroke-linecap="round" stroke-linejoin="round"/></svg></a> |
| 1568 | <ul> |
| 1569 | <li><a href="#" class="back-click <?php echo \esc_attr( self::$custom_class ); ?>"><?php echo \esc_html( self::$settings['parent'] ); ?></a></li> |
| 1570 | <li><?php echo \esc_html( self::$current_value ); ?></li> |
| 1571 | </ul> |
| 1572 | </nav> |
| 1573 | <?php |
| 1574 | } |
| 1575 | |
| 1576 | /** |
| 1577 | * Text input with a URL prefix displayed before it. |
| 1578 | * |
| 1579 | * Expected settings keys: |
| 1580 | * 'url_prefix' (string) - The URL prefix to display (e.g. site URL). |
| 1581 | * |
| 1582 | * @since 5.0.0 |
| 1583 | */ |
| 1584 | private static function url_prefixed_text() { |
| 1585 | $prefix = ! empty( self::$settings['url_prefix'] ) ? self::$settings['url_prefix'] : ''; |
| 1586 | ?> |
| 1587 | <div class="url-prefix-wrap"> |
| 1588 | <span class="url-prefix-text"><?php echo \esc_html( $prefix ); ?></span> |
| 1589 | <input <?php echo self::$item_id_attr; ?> <?php echo self::$name_attr; ?> type="text" value="<?php echo \esc_attr( self::$current_value ); ?>" class="regular-text<?php echo self::$custom_class; ?>" <?php echo self::$placeholder_attr; ?>> |
| 1590 | </div> |
| 1591 | <?php |
| 1592 | } |
| 1593 | |
| 1594 | /** |
| 1595 | * Prepare Data |
| 1596 | * |
| 1597 | * @param array $settings - Array with settings. |
| 1598 | * @param mixed $data - The data to show. |
| 1599 | * |
| 1600 | * @since 4.0.0 |
| 1601 | */ |
| 1602 | private static function prepare_data( $settings, $data ) { |
| 1603 | |
| 1604 | // Default Settings. |
| 1605 | $settings = \wp_parse_args( |
| 1606 | $settings, |
| 1607 | array( |
| 1608 | 'id' => '', |
| 1609 | 'class' => '', |
| 1610 | ) |
| 1611 | ); |
| 1612 | |
| 1613 | self::$settings = $settings; |
| 1614 | |
| 1615 | extract( $settings ); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract |
| 1616 | |
| 1617 | self::$option_type = ! empty( $type ) ? $type : false; |
| 1618 | self::$required = ! empty( $required ) ? $required : false; |
| 1619 | |
| 1620 | self::$min = ! empty( $min ) ? $min : null; |
| 1621 | self::$max = ! empty( $max ) ? $max : null; |
| 1622 | |
| 1623 | self::$step = ! empty( $step ) ? $step : null; |
| 1624 | |
| 1625 | self::$data_attrs = ! empty( $data_attrs ) ? $data_attrs : array(); |
| 1626 | |
| 1627 | if ( 'text' === self::$option_type || 'secret' === self::$option_type ) { |
| 1628 | self::$edit_type = ! empty( $validate ) ? $validate : false; |
| 1629 | self::$validate_pattern = ! empty( $pattern ) ? $pattern : false; |
| 1630 | self::$max_chars = ! empty( $max_chars ) ? $max_chars : false; |
| 1631 | self::$title_attr = ! empty( $title_attr ) ? $title_attr : false; |
| 1632 | } |
| 1633 | |
| 1634 | self::$not_bool = ! empty( $not_bool ) ? true : null; |
| 1635 | |
| 1636 | // ID. |
| 1637 | self::$item_id .= ! empty( $prefix ) ? $prefix . '-' : ''; |
| 1638 | self::$item_id .= ! empty( $id ) ? $id : ''; |
| 1639 | |
| 1640 | self::$item_id = \esc_attr( self::$item_id ); |
| 1641 | |
| 1642 | if ( ! empty( self::$item_id ) && ' ' !== self::$item_id ) { |
| 1643 | |
| 1644 | self::$item_id = ( 'arrayText' === $type ) ? self::$item_id . '-' . $key : self::$item_id; |
| 1645 | |
| 1646 | self::$item_id_attr = 'id="' . self::$item_id . '"'; |
| 1647 | self::$item_id_wrap = 'id="' . self::$item_id . '-item"'; |
| 1648 | } |
| 1649 | |
| 1650 | // Class. |
| 1651 | self::$custom_class = ! empty( $class ) ? ' ' . $class : ''; |
| 1652 | |
| 1653 | if ( isset( $option_name ) ) { |
| 1654 | $option_name = \esc_attr( $option_name ); |
| 1655 | } |
| 1656 | |
| 1657 | // Name. |
| 1658 | self::$name_attr = 'name="' . ( ( $option_name ) ?? '' ) . '"'; |
| 1659 | self::$option_name = ! empty( $option_name ) ? $option_name : null; |
| 1660 | |
| 1661 | // Placeholder. |
| 1662 | self::$placeholder_attr = ! empty( $placeholder ) ? 'placeholder="' . $placeholder . '"' : ''; |
| 1663 | |
| 1664 | // Get the option stored data. |
| 1665 | if ( ! \is_null( $data ) ) { |
| 1666 | self::$current_value = $data; |
| 1667 | } elseif ( isset( $default ) ) { |
| 1668 | self::$current_value = $default; |
| 1669 | } |
| 1670 | |
| 1671 | if ( empty( self::$current_value ) && ( 'list' === self::$option_type || 'tabbed-navigation' === self::$option_type ) ) { |
| 1672 | self::$current_value = array_map( |
| 1673 | function ( $value ) { |
| 1674 | $item = array( |
| 1675 | 'title' => ( $value['title'] ) ? (string) \esc_html( $value['title'] ) : 'Not Set', |
| 1676 | 'id' => ( $value['id'] ) ? (string) ( $value['id'] ) : '', |
| 1677 | ); |
| 1678 | if ( ! empty( $value['locked'] ) ) { |
| 1679 | $item['locked'] = true; |
| 1680 | } |
| 1681 | if ( ! empty( $value['premium_context'] ) ) { |
| 1682 | $item['premium_context'] = (string) $value['premium_context']; |
| 1683 | } |
| 1684 | return $item; |
| 1685 | }, |
| 1686 | $settings['values'] |
| 1687 | ); |
| 1688 | } |
| 1689 | } |
| 1690 | |
| 1691 | /** |
| 1692 | * Whether the sortable-list CSS/JS assets have been output. |
| 1693 | * |
| 1694 | * @var bool |
| 1695 | * |
| 1696 | * @since 3.1.2 |
| 1697 | */ |
| 1698 | private static $sortable_list_rendered = false; |
| 1699 | |
| 1700 | /** |
| 1701 | * Sortable List – renders an ordered list of items that can be reordered via drag-and-drop. |
| 1702 | * |
| 1703 | * Expected settings keys: |
| 1704 | * 'items' (array) - Ordered array of arrays with 'id', 'title', and optionally 'enabled' (bool), 'description' (string). |
| 1705 | * 'name_prefix' (string) - The hidden input name prefix for the order, e.g. 'wp_2fa_policy[methods_order]'. |
| 1706 | * Each item will produce: <input name="{name_prefix}[]" value="{id}">. |
| 1707 | * |
| 1708 | * @since 3.1.2 |
| 1709 | */ |
| 1710 | private static function sortable_list() { |
| 1711 | $items = ! empty( self::$settings['items'] ) ? self::$settings['items'] : array(); |
| 1712 | $name_prefix = ! empty( self::$settings['name_prefix'] ) ? self::$settings['name_prefix'] : ''; |
| 1713 | |
| 1714 | if ( ! self::$sortable_list_rendered ) { |
| 1715 | self::sortable_list_assets(); |
| 1716 | self::$sortable_list_rendered = true; |
| 1717 | } |
| 1718 | ?> |
| 1719 | <div <?php echo self::$item_id_wrap; ?> class="wp2fa-sortable-list-wrap <?php echo self::$custom_class; ?>"> |
| 1720 | <ul class="wp2fa-sortable-list" data-name-prefix="<?php echo \esc_attr( $name_prefix ); ?>"> |
| 1721 | <?php |
| 1722 | foreach ( $items as $item ) { |
| 1723 | $has_extra = ! empty( $item['extra_settings'] ); |
| 1724 | $has_integration = ! empty( $item['integration_settings'] ); |
| 1725 | $is_configured = ! empty( $item['is_configured'] ); |
| 1726 | $needs_setup = $has_integration && ! $is_configured; |
| 1727 | $li_class = 'wp2fa-sortable-item'; |
| 1728 | ?> |
| 1729 | <li class="<?php echo \esc_attr( $li_class ); ?>" data-id="<?php echo \esc_attr( $item['id'] ); ?>" <?php echo $needs_setup ? 'data-needs-setup="1"' : ''; ?>> |
| 1730 | <input type="hidden" name="<?php echo \esc_attr( $name_prefix ); ?>[]" value="<?php echo \esc_attr( ( $item['order_id'] ) ? $item['order_id'] : $item['id'] ); ?>"> |
| 1731 | <span class="wp2fa-sortable-handle" aria-label="<?php \esc_attr_e( 'Drag to reorder', 'wp-2fa' ); ?>">☰</span> |
| 1732 | <?php if ( ! empty( $item['checkbox_name'] ) ) { ?> |
| 1733 | <label class="wp2fa-sortable-checkbox toggle-switch"> |
| 1734 | <input type="checkbox" |
| 1735 | name="<?php echo \esc_attr( $item['checkbox_name'] ); ?>" |
| 1736 | value="<?php echo \esc_attr( ! empty( $item['checkbox_value'] ) ? $item['checkbox_value'] : $item['id'] ); ?>" |
| 1737 | <?php \checked( ! empty( $item['checked'] ) ); ?> |
| 1738 | <?php echo $has_extra ? 'data-has-extra="1"' : ''; ?>> |
| 1739 | <span class="toggle-slider"></span> |
| 1740 | <span class="wp2fa-sortable-title"> <?php echo \esc_html( $item['title'] ); ?></span> |
| 1741 | <?php if ( ! empty( $item['title_hint'] ) ) { ?> |
| 1742 | <span class="wp2fa-sortable-title-hint"><?php echo $item['title_hint']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Pre-escaped HTML with links. ?></span> |
| 1743 | <?php } ?> |
| 1744 | </label> |
| 1745 | <?php } else { ?> |
| 1746 | <span class="wp2fa-sortable-title"><?php echo \esc_html( $item['title'] ); ?></span> |
| 1747 | <?php } ?> |
| 1748 | <?php if ( ! empty( $item['setup_url'] ) ) { ?> |
| 1749 | <a href="<?php echo \esc_url( $item['setup_url'] ); ?>" class="wp2fa-sortable-setup-link" style="display:none;"><?php \esc_html_e( 'Setup method', 'wp-2fa' ); ?></a> |
| 1750 | <?php } ?> |
| 1751 | <?php if ( ! empty( $item['description'] ) ) { ?> |
| 1752 | <span class="wp2fa-sortable-desc"><?php echo $item['description']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Pre-escaped HTML with links. ?></span> |
| 1753 | <?php } ?> |
| 1754 | <?php if ( $has_extra ) { ?> |
| 1755 | <div class="wp2fa-method-settings-toggle-wrap" style="<?php echo empty( $item['checked'] ) ? 'display:none;' : ''; ?>"> |
| 1756 | <button type="button" class="wp2fa-collapsible-toggle" aria-expanded="false" data-label-show="<?php \esc_attr_e( 'Show method settings', 'wp-2fa' ); ?>" data-label-hide="<?php \esc_attr_e( 'Hide method settings', 'wp-2fa' ); ?>"> |
| 1757 | <span class="wp2fa-collapsible-chevron" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="7" viewBox="0 0 12 7" fill="none"><path d="M1 1L6 6L11 1" stroke="currentColor" stroke-width="1.875" stroke-linecap="round" stroke-linejoin="round"/></svg></span> |
| 1758 | <span class="wp2fa-collapsible-text"><?php \esc_html_e( 'Show method settings', 'wp-2fa' ); ?></span> |
| 1759 | </button> |
| 1760 | </div> |
| 1761 | <div class="wp2fa-sortable-extra-settings wp2fa-collapsible-content" style="display:none;"> |
| 1762 | <?php echo $item['extra_settings']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Pre-built HTML from Settings_Builder. ?> |
| 1763 | </div> |
| 1764 | <?php } ?> |
| 1765 | <?php if ( $has_integration ) { ?> |
| 1766 | <?php if ( ! $is_configured ) { ?> |
| 1767 | <div class="wp2fa-integration-setup-notice" style="display:none;"> |
| 1768 | <p><?php \esc_html_e( 'Complete configuration to enable this method', 'wp-2fa' ); ?></p> |
| 1769 | </div> |
| 1770 | <?php } ?> |
| 1771 | <div class="wp2fa-integration-settings-toggle-wrap" style="<?php echo ( empty( $item['checked'] ) && ! $needs_setup ) ? 'display:none;' : ''; ?>"> |
| 1772 | <button type="button" class="wp2fa-collapsible-toggle" aria-expanded="false" data-label-show="<?php \esc_attr_e( 'Show integration settings', 'wp-2fa' ); ?>" data-label-hide="<?php \esc_attr_e( 'Hide integration settings', 'wp-2fa' ); ?>"> |
| 1773 | <span class="wp2fa-collapsible-chevron" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="7" viewBox="0 0 12 7" fill="none"><path d="M1 1L6 6L11 1" stroke="currentColor" stroke-width="1.875" stroke-linecap="round" stroke-linejoin="round"/></svg></span> |
| 1774 | <span class="wp2fa-collapsible-text"><?php \esc_html_e( 'Show integration settings', 'wp-2fa' ); ?></span> |
| 1775 | </button> |
| 1776 | <span class="wp2fa-integration-status-dot <?php echo $is_configured ? 'wp2fa-status-configured' : 'wp2fa-status-not-configured'; ?>" title="<?php echo $is_configured ? \esc_attr__( 'Configured', 'wp-2fa' ) : \esc_attr__( 'Not configured', 'wp-2fa' ); ?>"></span> |
| 1777 | </div> |
| 1778 | <div class="wp2fa-sortable-integration-settings wp2fa-collapsible-content" style="display:none;"> |
| 1779 | <?php if ( ! $is_configured ) { ?> |
| 1780 | <div class="wp2fa-integration-info-notice"> |
| 1781 | <p><?php \esc_html_e( 'Configure this method below and hit save to enable it.', 'wp-2fa' ); ?></p> |
| 1782 | </div> |
| 1783 | <?php } ?> |
| 1784 | <?php echo $item['integration_settings']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Pre-built HTML from Settings_Builder. ?> |
| 1785 | </div> |
| 1786 | <?php } ?> |
| 1787 | </li> |
| 1788 | <?php } ?> |
| 1789 | </ul> |
| 1790 | </div> |
| 1791 | <?php |
| 1792 | } |
| 1793 | |
| 1794 | /** |
| 1795 | * Output inline CSS + JS for the sortable-list component. |
| 1796 | * |
| 1797 | * Uses vanilla JavaScript – no third-party libraries. |
| 1798 | * |
| 1799 | * @since 3.1.2 |
| 1800 | */ |
| 1801 | private static function sortable_list_assets() { |
| 1802 | ?> |
| 1803 | <style> |
| 1804 | /* .wp2fa-sortable-list-wrap { max-width: 600px; } */ |
| 1805 | .wp2fa-sortable-list { list-style: none; margin: 0; padding: 0; } |
| 1806 | .wp2fa-sortable-item { |
| 1807 | display: flex; align-items: center; gap: 8px; flex-wrap: wrap; |
| 1808 | background: #fff; border: 1px solid #e2e2e4; border-radius: 5px; |
| 1809 | padding: 12px 16px; margin-bottom: 8px; cursor: default; |
| 1810 | transition: box-shadow .15s, border-color .15s; |
| 1811 | user-select: none; |
| 1812 | } |
| 1813 | .wp2fa-sortable-item.wp2fa-sortable-dragging { |
| 1814 | opacity: .55; box-shadow: 0 4px 12px rgba(0,0,0,.15); border-color: #2271b1; |
| 1815 | } |
| 1816 | .wp2fa-sortable-item.wp2fa-sortable-over { |
| 1817 | border-top: 2px solid #2271b1; margin-top: -2px; |
| 1818 | } |
| 1819 | .wp2fa-sortable-handle { |
| 1820 | cursor: grab; font-size: 18px; color: #787c82; line-height: 1; flex-shrink: 0; |
| 1821 | } |
| 1822 | .wp2fa-sortable-handle:active { cursor: grabbing; } |
| 1823 | .wp2fa-sortable-checkbox { flex-shrink: 0; display: inline-flex; align-items: center; gap: 12px; } |
| 1824 | .wp2fa-sortable-checkbox input[type="checkbox"] { position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none; } |
| 1825 | .wp2fa-sortable-title { font-weight: 500; font-size: 14px; flex: 1; margin-left: 10px; } |
| 1826 | .wp2fa-sortable-desc { font-size: 12px; color: #646970; } |
| 1827 | .wp2fa-sortable-extra-settings { |
| 1828 | width: 100%; padding: 10px 0 4px 91px; border-top: 1px solid #f0f0f1; |
| 1829 | margin-top: 4px; display: flex; flex-direction: column; gap: 8px; |
| 1830 | } |
| 1831 | .wp2fa-sortable-extra-settings .number-input-wrap, |
| 1832 | .wp2fa-sortable-extra-settings .settings-control { margin: 0; } |
| 1833 | .wp2fa-sortable-extra-settings .number-input-wrap input[type="number"] { width: 70px; } |
| 1834 | .wp2fa-sortable-extra-settings .simple-text { font-size: 13px; margin: 0 0 4px; } |
| 1835 | .wp2fa-extra-inline { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } |
| 1836 | .wp2fa-extra-inline .settings-control { display: flex; align-items: center; gap: 6px; } |
| 1837 | .wp2fa-extra-inline .settings-control .radio-option { margin: 0; } |
| 1838 | .wp2fa-sortable-item-disabled { |
| 1839 | background: #f9f9f9; |
| 1840 | } |
| 1841 | .wp2fa-sortable-item-disabled .wp2fa-sortable-handle, |
| 1842 | .wp2fa-sortable-item-disabled .wp2fa-sortable-checkbox, |
| 1843 | .wp2fa-sortable-item-disabled .wp2fa-sortable-title { opacity: 0.5; pointer-events: none; } |
| 1844 | .wp2fa-sortable-setup-link { |
| 1845 | display: none; |
| 1846 | flex-shrink: 0; margin-left: auto; font-size: 13px; color: #2271b1; text-decoration: none; |
| 1847 | font-weight: 500; white-space: nowrap; |
| 1848 | } |
| 1849 | .wp2fa-sortable-setup-link:hover { text-decoration: underline; color: #135e96; } |
| 1850 | |
| 1851 | /* Collapsible toggle (Show/Hide method/integration settings) */ |
| 1852 | .wp2fa-method-settings-toggle-wrap, |
| 1853 | .wp2fa-integration-settings-toggle-wrap { |
| 1854 | width: 100%; padding: 8px 0 0 91px; display: flex; align-items: center; gap: 8px; |
| 1855 | } |
| 1856 | .wp2fa-collapsible-toggle { |
| 1857 | background: none; border: none; cursor: pointer; padding: 0; |
| 1858 | display: inline-flex; align-items: center; gap: 6px; |
| 1859 | font-size: 12px; font-weight: 600; text-transform: uppercase; |
| 1860 | color: #2271b1; letter-spacing: 0.3px; |
| 1861 | } |
| 1862 | .wp2fa-collapsible-toggle:hover { color: #135e96; } |
| 1863 | .wp2fa-collapsible-chevron { |
| 1864 | display: inline-flex; align-items: center; transition: transform 0.2s ease; |
| 1865 | } |
| 1866 | .wp2fa-collapsible-chevron svg { width: 12px; height: 7px; } |
| 1867 | .wp2fa-collapsible-toggle[aria-expanded="true"] .wp2fa-collapsible-chevron { |
| 1868 | transform: rotate(180deg); |
| 1869 | } |
| 1870 | .wp2fa-integration-status-dot { |
| 1871 | display: inline-block; width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; |
| 1872 | } |
| 1873 | .wp2fa-status-configured { background-color: #00a32a; } |
| 1874 | .wp2fa-status-not-configured { background-color: #a7aaad; } |
| 1875 | .wp2fa-sortable-integration-settings { |
| 1876 | width: 100%; padding: 10px 0 4px 91px; border-top: 1px solid #f0f0f1; |
| 1877 | margin-top: 4px; display: flex; flex-direction: column; gap: 8px; |
| 1878 | } |
| 1879 | .wp2fa-sortable-integration-settings .number-input-wrap, |
| 1880 | .wp2fa-sortable-integration-settings .settings-control { margin: 0; } |
| 1881 | .wp2fa-integration-setup-notice { |
| 1882 | width: 100%; padding: 8px 0 0 91px; |
| 1883 | } |
| 1884 | .wp2fa-integration-setup-notice p { |
| 1885 | background: #fcf0f1; border: 1px solid #f0c6c8; border-radius: 4px; |
| 1886 | padding: 12px 16px; color: #8b1a1f; font-size: 13px; line-height: 1.5; margin: 0; |
| 1887 | } |
| 1888 | .wp2fa-integration-info-notice { |
| 1889 | background: #fef3cd; border: 1px solid #ffc107; border-radius: 4px; |
| 1890 | padding: 14px 18px; color: #856404; font-size: 13px; line-height: 1.6; |
| 1891 | } |
| 1892 | .wp2fa-integration-info-notice p { margin: 0; } |
| 1893 | .wp2fa-sortable-desc { |
| 1894 | width: 100%; padding: 0 0 0 91px; font-size: 13px; color: #646970; line-height: 1.5; |
| 1895 | } |
| 1896 | .wp2fa-sortable-desc a { color: #2271b1; text-decoration: underline; } |
| 1897 | .wp2fa-sortable-desc a:hover { color: #135e96; } |
| 1898 | </style> |
| 1899 | <script> |
| 1900 | document.addEventListener('DOMContentLoaded', function() { |
| 1901 | /* Prevent enabling unconfigured integration methods – keep unchecked and show/hide integration settings */ |
| 1902 | document.querySelectorAll('.wp2fa-sortable-item[data-needs-setup="1"] input[type="checkbox"]').forEach(function(cb) { |
| 1903 | cb.addEventListener('change', function(e) { |
| 1904 | var li = this.closest('.wp2fa-sortable-item'); |
| 1905 | /* Only block if still needs setup (attribute may be removed after verification) */ |
| 1906 | if (this.checked && li.hasAttribute('data-needs-setup')) { |
| 1907 | /* Block the check */ |
| 1908 | this.checked = false; |
| 1909 | |
| 1910 | var setupNotice = li.querySelector('.wp2fa-integration-setup-notice'); |
| 1911 | var integrationWrap = li.querySelector('.wp2fa-integration-settings-toggle-wrap'); |
| 1912 | var integrationContent = li.querySelector('.wp2fa-sortable-integration-settings'); |
| 1913 | var integrationToggle = integrationWrap ? integrationWrap.querySelector('.wp2fa-collapsible-toggle') : null; |
| 1914 | |
| 1915 | /* If already expanded, collapse and return to original state */ |
| 1916 | if (integrationContent && integrationContent.style.display !== 'none') { |
| 1917 | if (setupNotice) { setupNotice.style.display = 'none'; } |
| 1918 | if (integrationWrap) { integrationWrap.style.display = 'none'; } |
| 1919 | integrationContent.style.display = 'none'; |
| 1920 | if (integrationToggle) { |
| 1921 | integrationToggle.setAttribute('aria-expanded', 'false'); |
| 1922 | integrationToggle.querySelector('.wp2fa-collapsible-text').textContent = integrationToggle.dataset.labelShow; |
| 1923 | } |
| 1924 | } else { |
| 1925 | /* Show the setup notice */ |
| 1926 | if (setupNotice) { setupNotice.style.display = ''; } |
| 1927 | /* Show the integration settings toggle and expand it */ |
| 1928 | if (integrationWrap) { integrationWrap.style.display = ''; } |
| 1929 | if (integrationContent && integrationToggle) { |
| 1930 | integrationContent.style.display = ''; |
| 1931 | integrationToggle.setAttribute('aria-expanded', 'true'); |
| 1932 | integrationToggle.querySelector('.wp2fa-collapsible-text').textContent = integrationToggle.dataset.labelHide; |
| 1933 | } |
| 1934 | } |
| 1935 | } |
| 1936 | }); |
| 1937 | }); |
| 1938 | |
| 1939 | /* Toggle extra/integration settings visibility on checkbox change */ |
| 1940 | document.querySelectorAll('.wp2fa-sortable-item input[type="checkbox"][data-has-extra]').forEach(function(cb) { |
| 1941 | cb.addEventListener('change', function() { |
| 1942 | var li = this.closest('.wp2fa-sortable-item'); |
| 1943 | var toggleWrap = li.querySelector('.wp2fa-method-settings-toggle-wrap'); |
| 1944 | if (toggleWrap) { toggleWrap.style.display = this.checked ? '' : 'none'; } |
| 1945 | var integrationWrap = li.querySelector('.wp2fa-integration-settings-toggle-wrap'); |
| 1946 | if (integrationWrap) { integrationWrap.style.display = this.checked ? '' : 'none'; } |
| 1947 | /* Hide the collapsible content when method is unchecked */ |
| 1948 | if (!this.checked) { |
| 1949 | var setupNotice = li.querySelector('.wp2fa-integration-setup-notice'); |
| 1950 | if (setupNotice) { setupNotice.style.display = 'none'; } |
| 1951 | li.querySelectorAll('.wp2fa-collapsible-content').forEach(function(content) { |
| 1952 | content.style.display = 'none'; |
| 1953 | }); |
| 1954 | li.querySelectorAll('.wp2fa-collapsible-toggle').forEach(function(toggle) { |
| 1955 | toggle.setAttribute('aria-expanded', 'false'); |
| 1956 | toggle.querySelector('.wp2fa-collapsible-text').textContent = toggle.dataset.labelShow; |
| 1957 | }); |
| 1958 | } |
| 1959 | }); |
| 1960 | }); |
| 1961 | |
| 1962 | /* Also handle integration toggle visibility on checkbox change (methods without extra_settings) */ |
| 1963 | document.querySelectorAll('.wp2fa-sortable-item:not([data-needs-setup]) input[type="checkbox"]').forEach(function(cb) { |
| 1964 | if (cb.dataset.hasExtra) return; /* Already handled above */ |
| 1965 | cb.addEventListener('change', function() { |
| 1966 | var li = this.closest('.wp2fa-sortable-item'); |
| 1967 | var integrationWrap = li.querySelector('.wp2fa-integration-settings-toggle-wrap'); |
| 1968 | if (integrationWrap) { integrationWrap.style.display = this.checked ? '' : 'none'; } |
| 1969 | if (!this.checked) { |
| 1970 | var setupNotice = li.querySelector('.wp2fa-integration-setup-notice'); |
| 1971 | if (setupNotice) { setupNotice.style.display = 'none'; } |
| 1972 | li.querySelectorAll('.wp2fa-collapsible-content').forEach(function(content) { |
| 1973 | content.style.display = 'none'; |
| 1974 | }); |
| 1975 | li.querySelectorAll('.wp2fa-collapsible-toggle').forEach(function(toggle) { |
| 1976 | toggle.setAttribute('aria-expanded', 'false'); |
| 1977 | toggle.querySelector('.wp2fa-collapsible-text').textContent = toggle.dataset.labelShow; |
| 1978 | }); |
| 1979 | } |
| 1980 | }); |
| 1981 | }); |
| 1982 | |
| 1983 | /* Collapsible toggle click handler */ |
| 1984 | document.querySelectorAll('.wp2fa-collapsible-toggle').forEach(function(btn) { |
| 1985 | btn.addEventListener('click', function(e) { |
| 1986 | e.preventDefault(); |
| 1987 | var li = this.closest('.wp2fa-sortable-item'); |
| 1988 | var isIntegration = this.closest('.wp2fa-integration-settings-toggle-wrap') !== null; |
| 1989 | var content = isIntegration |
| 1990 | ? li.querySelector('.wp2fa-sortable-integration-settings') |
| 1991 | : li.querySelector('.wp2fa-sortable-extra-settings'); |
| 1992 | if (!content) return; |
| 1993 | var isExpanded = this.getAttribute('aria-expanded') === 'true'; |
| 1994 | if (isExpanded) { |
| 1995 | content.style.display = 'none'; |
| 1996 | this.setAttribute('aria-expanded', 'false'); |
| 1997 | this.querySelector('.wp2fa-collapsible-text').textContent = this.dataset.labelShow; |
| 1998 | if (isIntegration) { |
| 1999 | var setupNotice = li.querySelector('.wp2fa-integration-setup-notice'); |
| 2000 | if (setupNotice) { setupNotice.style.display = 'none'; } |
| 2001 | } |
| 2002 | } else { |
| 2003 | content.style.display = ''; |
| 2004 | this.setAttribute('aria-expanded', 'true'); |
| 2005 | this.querySelector('.wp2fa-collapsible-text').textContent = this.dataset.labelHide; |
| 2006 | if (isIntegration) { |
| 2007 | var setupNotice = li.querySelector('.wp2fa-integration-setup-notice'); |
| 2008 | if (setupNotice) { setupNotice.style.display = ''; } |
| 2009 | } |
| 2010 | } |
| 2011 | }); |
| 2012 | }); |
| 2013 | |
| 2014 | document.querySelectorAll('.wp2fa-sortable-list').forEach(function(list) { |
| 2015 | var dragItem = null; |
| 2016 | |
| 2017 | list.querySelectorAll('.wp2fa-sortable-item').forEach(function(item) { |
| 2018 | var handle = item.querySelector('.wp2fa-sortable-handle'); |
| 2019 | if (!handle) return; |
| 2020 | |
| 2021 | handle.addEventListener('mousedown', function() { |
| 2022 | item.setAttribute('draggable', 'true'); |
| 2023 | }); |
| 2024 | handle.addEventListener('mouseup', function() { |
| 2025 | item.removeAttribute('draggable'); |
| 2026 | }); |
| 2027 | |
| 2028 | item.addEventListener('dragstart', function(e) { |
| 2029 | dragItem = item; |
| 2030 | item.classList.add('wp2fa-sortable-dragging'); |
| 2031 | e.dataTransfer.effectAllowed = 'move'; |
| 2032 | e.dataTransfer.setData('text/plain', ''); |
| 2033 | }); |
| 2034 | |
| 2035 | item.addEventListener('dragend', function() { |
| 2036 | item.classList.remove('wp2fa-sortable-dragging'); |
| 2037 | item.removeAttribute('draggable'); |
| 2038 | dragItem = null; |
| 2039 | list.querySelectorAll('.wp2fa-sortable-item').forEach(function(el) { |
| 2040 | el.classList.remove('wp2fa-sortable-over'); |
| 2041 | }); |
| 2042 | /* Rebuild hidden inputs order */ |
| 2043 | var prefix = list.getAttribute('data-name-prefix'); |
| 2044 | list.querySelectorAll('.wp2fa-sortable-item').forEach(function(li) { |
| 2045 | var inp = li.querySelector('input[type="hidden"]'); |
| 2046 | if (inp) inp.setAttribute('name', prefix + '[]'); |
| 2047 | }); |
| 2048 | }); |
| 2049 | |
| 2050 | item.addEventListener('dragover', function(e) { |
| 2051 | e.preventDefault(); |
| 2052 | e.dataTransfer.dropEffect = 'move'; |
| 2053 | if (item !== dragItem) { |
| 2054 | item.classList.add('wp2fa-sortable-over'); |
| 2055 | } |
| 2056 | }); |
| 2057 | |
| 2058 | item.addEventListener('dragleave', function() { |
| 2059 | item.classList.remove('wp2fa-sortable-over'); |
| 2060 | }); |
| 2061 | |
| 2062 | item.addEventListener('drop', function(e) { |
| 2063 | e.preventDefault(); |
| 2064 | item.classList.remove('wp2fa-sortable-over'); |
| 2065 | if (dragItem && dragItem !== item) { |
| 2066 | var allItems = Array.from(list.children); |
| 2067 | var dragIdx = allItems.indexOf(dragItem); |
| 2068 | var dropIdx = allItems.indexOf(item); |
| 2069 | if (dragIdx < dropIdx) { |
| 2070 | list.insertBefore(dragItem, item.nextSibling); |
| 2071 | } else { |
| 2072 | list.insertBefore(dragItem, item); |
| 2073 | } |
| 2074 | } |
| 2075 | }); |
| 2076 | |
| 2077 | /* Touch support for mobile */ |
| 2078 | var touchStartY = 0; |
| 2079 | var touchClone = null; |
| 2080 | var touchCurrentOver = null; |
| 2081 | |
| 2082 | handle.addEventListener('touchstart', function(e) { |
| 2083 | dragItem = item; |
| 2084 | touchStartY = e.touches[0].clientY; |
| 2085 | item.classList.add('wp2fa-sortable-dragging'); |
| 2086 | e.preventDefault(); |
| 2087 | }, { passive: false }); |
| 2088 | |
| 2089 | handle.addEventListener('touchmove', function(e) { |
| 2090 | if (!dragItem) return; |
| 2091 | e.preventDefault(); |
| 2092 | var touch = e.touches[0]; |
| 2093 | var overEl = document.elementFromPoint(touch.clientX, touch.clientY); |
| 2094 | if (overEl) { |
| 2095 | var overItem = overEl.closest('.wp2fa-sortable-item'); |
| 2096 | if (touchCurrentOver && touchCurrentOver !== overItem) { |
| 2097 | touchCurrentOver.classList.remove('wp2fa-sortable-over'); |
| 2098 | } |
| 2099 | if (overItem && overItem !== dragItem && overItem.parentNode === list) { |
| 2100 | overItem.classList.add('wp2fa-sortable-over'); |
| 2101 | touchCurrentOver = overItem; |
| 2102 | } |
| 2103 | } |
| 2104 | }, { passive: false }); |
| 2105 | |
| 2106 | handle.addEventListener('touchend', function(e) { |
| 2107 | if (!dragItem) return; |
| 2108 | dragItem.classList.remove('wp2fa-sortable-dragging'); |
| 2109 | if (touchCurrentOver) { |
| 2110 | touchCurrentOver.classList.remove('wp2fa-sortable-over'); |
| 2111 | var allItems = Array.from(list.children); |
| 2112 | var dragIdx = allItems.indexOf(dragItem); |
| 2113 | var dropIdx = allItems.indexOf(touchCurrentOver); |
| 2114 | if (dragIdx < dropIdx) { |
| 2115 | list.insertBefore(dragItem, touchCurrentOver.nextSibling); |
| 2116 | } else { |
| 2117 | list.insertBefore(dragItem, touchCurrentOver); |
| 2118 | } |
| 2119 | /* Rebuild hidden inputs */ |
| 2120 | var prefix = list.getAttribute('data-name-prefix'); |
| 2121 | list.querySelectorAll('.wp2fa-sortable-item').forEach(function(li) { |
| 2122 | var inp = li.querySelector('input[type="hidden"]'); |
| 2123 | if (inp) inp.setAttribute('name', prefix + '[]'); |
| 2124 | }); |
| 2125 | } |
| 2126 | dragItem = null; |
| 2127 | touchCurrentOver = null; |
| 2128 | }); |
| 2129 | }); |
| 2130 | }); |
| 2131 | }); |
| 2132 | </script> |
| 2133 | <?php |
| 2134 | } |
| 2135 | |
| 2136 | /** |
| 2137 | * Creates an option and draws it |
| 2138 | * |
| 2139 | * @param array $value - The array with option data. |
| 2140 | * |
| 2141 | * @return void |
| 2142 | * |
| 2143 | * @since 4.0.0 |
| 2144 | */ |
| 2145 | public static function build_option( array $value ) { |
| 2146 | $data = null; |
| 2147 | |
| 2148 | if ( empty( $value['id'] ) ) { |
| 2149 | $value['id'] = ' '; |
| 2150 | } |
| 2151 | |
| 2152 | self::create( $value, $data ); |
| 2153 | } |
| 2154 | |
| 2155 | /** |
| 2156 | * Renders the premium badge (lock icon + "PREMIUM" label). |
| 2157 | * |
| 2158 | * Use this inline next to feature labels that require a premium license. |
| 2159 | * |
| 2160 | * @return void |
| 2161 | * |
| 2162 | * @since 4.1.0 |
| 2163 | */ |
| 2164 | public static function premium_badge() { |
| 2165 | echo self::get_premium_badge_html(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped |
| 2166 | } |
| 2167 | |
| 2168 | /** |
| 2169 | * Returns the premium badge HTML markup (lock icon + "PREMIUM" label). |
| 2170 | * |
| 2171 | * Useful for embedding in other element attributes like checkbox text. |
| 2172 | * |
| 2173 | * @return string |
| 2174 | * |
| 2175 | * @since 4.1.0 |
| 2176 | */ |
| 2177 | public static function get_premium_badge_html( string $context = '' ): string { |
| 2178 | return self::get_plan_badge_html( \esc_html__( 'PREMIUM', 'wp-2fa' ), $context ); |
| 2179 | } |
| 2180 | |
| 2181 | /** |
| 2182 | * Renders the enterprise badge (lock icon + "ENTERPRISE" label). |
| 2183 | * |
| 2184 | * Use this inline next to feature labels that require an enterprise license. |
| 2185 | * |
| 2186 | * @return void |
| 2187 | * |
| 2188 | * @since 4.1.0 |
| 2189 | */ |
| 2190 | public static function enterprise_badge() { |
| 2191 | echo self::get_enterprise_badge_html(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped |
| 2192 | } |
| 2193 | |
| 2194 | /** |
| 2195 | * Returns the enterprise badge HTML markup (lock icon + "ENTERPRISE" label). |
| 2196 | * |
| 2197 | * Useful for embedding in other element attributes like checkbox text. |
| 2198 | * |
| 2199 | * @return string |
| 2200 | * |
| 2201 | * @since 4.1.0 |
| 2202 | */ |
| 2203 | public static function get_enterprise_badge_html( string $context = '' ): string { |
| 2204 | return self::get_plan_badge_html( \esc_html__( 'ENTERPRISE', 'wp-2fa' ), $context ); |
| 2205 | } |
| 2206 | |
| 2207 | /** |
| 2208 | * Returns a badge HTML markup for a license plan label. |
| 2209 | * |
| 2210 | * @param string $label Badge text label. |
| 2211 | * |
| 2212 | * @return string |
| 2213 | * |
| 2214 | * @since 4.1.0 |
| 2215 | */ |
| 2216 | private static function get_plan_badge_html( string $label, string $context = '' ): string { |
| 2217 | $aria_label = sprintf( |
| 2218 | /* translators: %s: plan label. */ |
| 2219 | esc_html__( 'Learn more about %s features', 'wp-2fa' ), |
| 2220 | wp_strip_all_tags( $label ) |
| 2221 | ); |
| 2222 | |
| 2223 | $context_attr = '' !== $context ? ' data-wp2fa-premium-context="' . esc_attr( $context ) . '"' : ''; |
| 2224 | |
| 2225 | return '<span class="wp2fa-premium-badge" role="button" tabindex="0" data-wp2fa-premium-dialog-trigger="1"' . $context_attr . ' aria-label="' . esc_attr( $aria_label ) . '">' |
| 2226 | . '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">' |
| 2227 | . '<path d="M4.66699 6.41667V4.08333C4.66699 3.46449 4.91282 2.871 5.35041 2.43342C5.78799 1.99583 6.38149 1.75 7.00033 1.75C7.61916 1.75 8.21266 1.99583 8.65024 2.43342C9.08783 2.871 9.33366 3.46449 9.33366 4.08333V6.41667M2.91699 7.58333C2.91699 7.27391 3.03991 6.97717 3.2587 6.75838C3.47749 6.53958 3.77424 6.41667 4.08366 6.41667H9.91699C10.2264 6.41667 10.5232 6.53958 10.742 6.75838C10.9607 6.97717 11.0837 7.27391 11.0837 7.58333V11.0833C11.0837 11.3928 10.9607 11.6895 10.742 11.9083C10.5232 12.1271 10.2264 12.25 9.91699 12.25H4.08366C3.77424 12.25 3.47749 12.1271 3.2587 11.9083C3.03991 11.6895 2.91699 11.3928 2.91699 11.0833V7.58333ZM6.41699 9.33333C6.41699 9.48804 6.47845 9.63642 6.58785 9.74581C6.69724 9.85521 6.84562 9.91667 7.00033 9.91667C7.15504 9.91667 7.30341 9.85521 7.4128 9.74581C7.5222 9.63642 7.58366 9.48804 7.58366 9.33333C7.58366 9.17862 7.5222 9.03025 7.4128 8.92085C7.30341 8.81146 7.15504 8.75 7.00033 8.75C6.84562 8.75 6.69724 8.81146 6.58785 8.92085C6.47845 9.03025 6.41699 9.17862 6.41699 9.33333Z" stroke="#646970" stroke-width="1.3125" stroke-linecap="round" stroke-linejoin="round"/>' |
| 2228 | . '</svg> ' |
| 2229 | . esc_html( $label ) |
| 2230 | . '</span>'; |
| 2231 | } |
| 2232 | |
| 2233 | /** |
| 2234 | * Opens a premium-gated wrapper. |
| 2235 | * |
| 2236 | * When $is_premium is false the wrapper disables all contained inputs |
| 2237 | * and applies premium styling. Call premium_gate_close() to end the block. |
| 2238 | * |
| 2239 | * @param bool $is_premium Whether the current install has a valid premium license. |
| 2240 | * |
| 2241 | * @return void |
| 2242 | * |
| 2243 | * @since 4.1.0 |
| 2244 | */ |
| 2245 | public static function premium_gate_open( bool $is_premium ) { |
| 2246 | $class = $is_premium ? '' : ' wp2fa-premium-gate-locked'; |
| 2247 | ?> |
| 2248 | <div class="wp2fa-premium-gate<?php echo \esc_attr( $class ); ?>"> |
| 2249 | <?php |
| 2250 | } |
| 2251 | |
| 2252 | /** |
| 2253 | * Closes a premium-gated wrapper. |
| 2254 | * |
| 2255 | * @return void |
| 2256 | * |
| 2257 | * @since 4.1.0 |
| 2258 | */ |
| 2259 | public static function premium_gate_close() { |
| 2260 | ?> |
| 2261 | </div> |
| 2262 | <?php |
| 2263 | } |
| 2264 | } |
| 2265 | } |
| 2266 |