| @@ -73,9 +73,9 @@ | ||
| 73 | 73 | * |
| 74 | 74 | * @return string Description of the field. |
| 75 | 75 | */ |
| 76 | 76 | public function get_field_description( $args ) { |
| 77 | - $desc = ! empty( $args['desc'] ) ? '<p class="description">' . wp_kses_post( $args['desc'] ) . '</p>' : ''; | |
| 77 | + $desc = ! empty( $args['desc'] ) ? '<p class="description">' . wp_kses( $args['desc'], $this->get_allowed_html() ) . '</p>' : ''; | |
| 78 | 78 | |
| 79 | 79 | /** |
| 80 | 80 | * After Settings Output filter |
| 81 | 81 | * |
| @@ -106,9 +106,9 @@ | ||
| 106 | 106 | $default = $this->get_field_default( $args ); |
| 107 | 107 | if ( is_array( $default ) ) { |
| 108 | 108 | $default = implode( ',', array_map( 'strval', $default ) ); |
| 109 | 109 | } |
| 110 | - $default = trim( (string) $default ); | |
| 110 | + $default = trim( (string) $default, " \t\n\r\0\x0B" ); | |
| 111 | 111 | |
| 112 | 112 | // Map option keys to their labels where the field has a choices list. |
| 113 | 113 | if ( ! empty( $args['options'] ) && is_array( $args['options'] ) ) { |
| 114 | 114 | if ( 'radiodesc' === $type && '' !== $default ) { |
| @@ -124,9 +124,9 @@ | ||
| 124 | 124 | } |
| 125 | 125 | } elseif ( '' !== $default && 'multicheck' === $type ) { |
| 126 | 126 | $labels = array(); |
| 127 | 127 | foreach ( explode( ',', $default ) as $key ) { |
| 128 | - $key = trim( $key ); | |
| 128 | + $key = trim( $key, " \t\n\r\0\x0B" ); | |
| 129 | 129 | $labels[] = isset( $args['options'][ $key ] ) && is_string( $args['options'][ $key ] ) ? $args['options'][ $key ] : $key; |
| 130 | 130 | } |
| 131 | 131 | $default = implode( ', ', $labels ); |
| 132 | 132 | } |
| @@ -196,9 +196,9 @@ | ||
| 196 | 196 | if ( is_array( $value ) || is_array( $default ) || in_array( $type, array( 'multicheck', 'posttypes', 'taxonomies' ), true ) ) { |
| 197 | 197 | return $this->normalize_list_value( $value ) !== $this->normalize_list_value( $default ); |
| 198 | 198 | } |
| 199 | 199 | |
| 200 | - return trim( (string) $value ) !== trim( (string) $default ); | |
| 200 | + return trim( (string) $value, " \t\n\r\0\x0B" ) !== trim( (string) $default, " \t\n\r\0\x0B" ); | |
| 201 | 201 | } |
| 202 | 202 | |
| 203 | 203 | /** |
| 204 | 204 | * Normalize a list-style value (array or comma-separated string) into a |
| @@ -280,9 +280,9 @@ | ||
| 280 | 280 | $class = implode( ' ', array_map( 'sanitize_html_class', explode( ' ', $args['field_class'] ?? '' ) ) ); |
| 281 | 281 | if ( $default_class ) { |
| 282 | 282 | $class = $default_class . ' ' . $class; |
| 283 | 283 | } |
| 284 | - return trim( $class ); | |
| 284 | + return trim( $class, " \t\n\r\0\x0B" ); | |
| 285 | 285 | } |
| 286 | 286 | |
| 287 | 287 | /** |
| 288 | 288 | * Get placeholder attribute string. |
| @@ -300,9 +300,9 @@ | ||
| 300 | 300 | * @param array $args Field arguments. |
| 301 | 301 | * @return string Concatenated boolean attributes. |
| 302 | 302 | */ |
| 303 | 303 | protected function get_boolean_attributes( $args ) { |
| 304 | - $disabled = ( ! empty( $args['disabled'] ) || ! empty( $args['pro'] ) ) ? ' disabled="disabled"' : ''; | |
| 304 | + $disabled = $this->is_field_disabled( $args ) ? ' disabled="disabled"' : ''; | |
| 305 | 305 | $readonly = ( isset( $args['readonly'] ) && true === $args['readonly'] ) ? ' readonly="readonly"' : ''; |
| 306 | 306 | $required = ( isset( $args['required'] ) && true === $args['required'] ) ? ' required' : ''; |
| 307 | 307 | return $disabled . $readonly . $required; |
| 308 | 308 | } |
| @@ -307,8 +307,18 @@ | ||
| 307 | 307 | return $disabled . $readonly . $required; |
| 308 | 308 | } |
| 309 | 309 | |
| 310 | 310 | /** |
| 311 | + * Whether a field is disabled, either explicitly or because it is a pro-only field. | |
| 312 | + * | |
| 313 | + * @param array $args Field arguments. | |
| 314 | + * @return bool Whether the field is disabled. | |
| 315 | + */ | |
| 316 | + protected function is_field_disabled( $args ) { | |
| 317 | + return ! empty( $args['disabled'] ) || ! empty( $args['pro'] ); | |
| 318 | + } | |
| 319 | + | |
| 320 | + /** | |
| 311 | 321 | * Get field ID and name attributes. |
| 312 | 322 | * |
| 313 | 323 | * @param array $args Field arguments. |
| 314 | 324 | * @return array Array containing field_id and field_name. |
| @@ -341,8 +351,20 @@ | ||
| 341 | 351 | ); |
| 342 | 352 | } |
| 343 | 353 | |
| 344 | 354 | /** |
| 355 | + * Get the rendered field ID for a field definition. | |
| 356 | + * | |
| 357 | + * @param array $args Field arguments. | |
| 358 | + * @return string Field ID. | |
| 359 | + */ | |
| 360 | + public function get_field_id( $args ) { | |
| 361 | + $field_attributes = $this->get_field_attributes( $args ); | |
| 362 | + | |
| 363 | + return $field_attributes['field_id']; | |
| 364 | + } | |
| 365 | + | |
| 366 | + /** | |
| 345 | 367 | * Returns the allowed HTML tags and attributes for settings form output. |
| 346 | 368 | * |
| 347 | 369 | * Use the `{prefix}_settings_form_allowed_html` filter to add extra tags or |
| 348 | 370 | * attributes needed by custom field types or `field_attributes` entries. |
| @@ -423,14 +445,33 @@ | ||
| 423 | 445 | 'class' => true, |
| 424 | 446 | 'style' => true, |
| 425 | 447 | ), |
| 426 | 448 | 'button' => array( |
| 427 | - 'type' => true, | |
| 428 | - 'id' => true, | |
| 429 | - 'class' => true, | |
| 430 | - 'style' => true, | |
| 431 | - 'disabled' => true, | |
| 449 | + 'type' => true, | |
| 450 | + 'id' => true, | |
| 451 | + 'class' => true, | |
| 452 | + 'style' => true, | |
| 453 | + 'disabled' => true, | |
| 454 | + 'aria-controls' => true, | |
| 455 | + 'aria-expanded' => true, | |
| 456 | + 'aria-label' => true, | |
| 432 | 457 | ), |
| 458 | + 'div' => array( | |
| 459 | + 'id' => true, | |
| 460 | + 'class' => true, | |
| 461 | + 'style' => true, | |
| 462 | + 'aria-hidden' => true, | |
| 463 | + 'aria-busy' => true, | |
| 464 | + 'role' => true, | |
| 465 | + ), | |
| 466 | + 'span' => array( | |
| 467 | + 'class' => true, | |
| 468 | + 'style' => true, | |
| 469 | + 'aria-hidden' => true, | |
| 470 | + 'aria-live' => true, | |
| 471 | + 'aria-atomic' => true, | |
| 472 | + 'role' => true, | |
| 473 | + ), | |
| 433 | 474 | 'template' => array( |
| 434 | 475 | 'class' => true, |
| 435 | 476 | 'data-id' => true, |
| 436 | 477 | ), |
| @@ -545,8 +586,34 @@ | ||
| 545 | 586 | echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound |
| 546 | 587 | } |
| 547 | 588 | |
| 548 | 589 | /** |
| 590 | + * Display local date/time fields. | |
| 591 | + * | |
| 592 | + * @param array $args Field arguments. | |
| 593 | + */ | |
| 594 | + public function callback_datetime( $args ) { | |
| 595 | + $value = $this->get_field_value( $args ); | |
| 596 | + $class = $this->get_field_class( $args ); | |
| 597 | + $attributes = $this->get_boolean_attributes( $args ) . $this->build_field_attributes( $args ); | |
| 598 | + $placeholder = $this->get_placeholder_attribute( $args ); | |
| 599 | + $field_attributes = $this->get_field_attributes( $args ); | |
| 600 | + | |
| 601 | + $html = sprintf( | |
| 602 | + '<input type="datetime-local" id="%1$s" name="%2$s" class="%3$s" value="%4$s" step="60" %5$s %6$s />', | |
| 603 | + $field_attributes['field_id'], | |
| 604 | + $field_attributes['field_name'], | |
| 605 | + $class . ' regular-text', | |
| 606 | + esc_attr( stripslashes( (string) $value ) ), | |
| 607 | + $attributes, | |
| 608 | + $placeholder | |
| 609 | + ); | |
| 610 | + $html .= $this->get_field_description( $args ); | |
| 611 | + | |
| 612 | + echo wp_kses( apply_filters( $this->prefix . '_after_setting_output', $html, $args ), $this->get_allowed_html() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound | |
| 613 | + } | |
| 614 | + | |
| 615 | + /** | |
| 549 | 616 | * Display url fields. |
| 550 | 617 | * |
| 551 | 618 | * @param array $args Array of arguments. |
| 552 | 619 | */ |
| @@ -649,9 +716,9 @@ | ||
| 649 | 716 | * @param array $args Field arguments. |
| 650 | 717 | * @return string Disabled attribute or empty string. |
| 651 | 718 | */ |
| 652 | 719 | protected function get_disabled_attribute( $args ) { |
| 653 | - return ( ! empty( $args['disabled'] ) || ! empty( $args['pro'] ) ) ? ' disabled="disabled"' : ''; | |
| 720 | + return $this->is_field_disabled( $args ) ? ' disabled="disabled"' : ''; | |
| 654 | 721 | } |
| 655 | 722 | |
| 656 | 723 | /** |
| 657 | 724 | * Display checkboxes. |
| @@ -929,9 +996,9 @@ | ||
| 929 | 996 | public function callback_number( $args ) { |
| 930 | 997 | $value = $this->get_field_value( $args ); |
| 931 | 998 | $max = isset( $args['max'] ) ? intval( $args['max'] ) : 999999; |
| 932 | 999 | $min = isset( $args['min'] ) ? intval( $args['min'] ) : 0; |
| 933 | - $step = isset( $args['step'] ) ? intval( $args['step'] ) : 1; | |
| 1000 | + $step = isset( $args['step'] ) && ( is_numeric( $args['step'] ) || 'any' === $args['step'] ) ? $args['step'] : 1; | |
| 934 | 1001 | $size = $args['size'] ?? 'regular'; |
| 935 | 1002 | $placeholder = $this->get_placeholder_attribute( $args ); |
| 936 | 1003 | $attributes = $this->get_boolean_attributes( $args ); |
| 937 | 1004 | |
| @@ -1216,8 +1283,13 @@ | ||
| 1216 | 1283 | |
| 1217 | 1284 | $class = $this->get_field_class( $args ); |
| 1218 | 1285 | $attributes = $this->get_boolean_attributes( $args ) . $this->build_field_attributes( $args ); |
| 1219 | 1286 | |
| 1287 | + // `disabled` does nothing on the wrapper; the class neutralises the buttons. | |
| 1288 | + if ( $this->is_field_disabled( $args ) ) { | |
| 1289 | + $class .= ' wz-repeater-disabled'; | |
| 1290 | + } | |
| 1291 | + | |
| 1220 | 1292 | $data_index = (string) count( $value ); |
| 1221 | 1293 | $live_update_field = ! empty( $args['live_update_field'] ) ? $args['live_update_field'] : 'name'; |
| 1222 | 1294 | $fallback_title = ! empty( $args['new_item_text'] ) ? $args['new_item_text'] : $this->translation_strings['repeater_new_item']; |
| 1223 | 1295 | $live_update_options = ! empty( $args['live_update_field_options'] ) && is_array( $args['live_update_field_options'] ) ? $args['live_update_field_options'] : array(); |
| @@ -1245,9 +1317,9 @@ | ||
| 1245 | 1317 | } |
| 1246 | 1318 | } |
| 1247 | 1319 | ?> |
| 1248 | 1320 | </div> |
| 1249 | - <button type="button" class="button add-item" data-target="<?php echo esc_attr( $args['id'] ); ?>"> | |
| 1321 | + <button type="button" class="button add-item" data-target="<?php echo esc_attr( $args['id'] ); ?>" <?php disabled( $this->is_field_disabled( $args ) ); ?>> | |
| 1250 | 1322 | <?php echo esc_html( ! empty( $args['add_button_text'] ) ? $args['add_button_text'] : 'Add Item' ); ?> |
| 1251 | 1323 | </button> |
| 1252 | 1324 | |
| 1253 | 1325 | <template class="repeater-template" data-id="<?php echo esc_attr( $args['id'] ); ?>"> |
| @@ -1290,12 +1362,19 @@ | ||
| 1290 | 1362 | // For new items, use a placeholder that will be replaced. |
| 1291 | 1363 | $item_id = '{{ROW_ID}}'; |
| 1292 | 1364 | } |
| 1293 | 1365 | |
| 1366 | + $parent_disabled = $this->is_field_disabled( $args ); | |
| 1367 | + $content_id = sprintf( | |
| 1368 | + '%s-%s-%s-content', | |
| 1369 | + sanitize_key( $this->settings_key ), | |
| 1370 | + sanitize_key( $args['id'] ), | |
| 1371 | + '{{ROW_ID}}' === $item_id ? '{{ROW_ID}}' : sanitize_key( $item_id ) | |
| 1372 | + ); | |
| 1294 | 1373 | ?> |
| 1295 | 1374 | <div class="wz-repeater-item" data-row-id="<?php echo esc_attr( $item_id ); ?>"> |
| 1296 | - <input type="hidden" name="<?php echo esc_attr( $this->settings_key ); ?>[<?php echo esc_attr( $args['id'] ); ?>][<?php echo esc_attr( $index ); ?>][row_id]" value="<?php echo esc_attr( $item_id ); ?>" /> | |
| 1297 | - <div class="repeater-item-header"> | |
| 1375 | + <input type="hidden" name="<?php echo esc_attr( $this->settings_key ); ?>[<?php echo esc_attr( $args['id'] ); ?>][<?php echo esc_attr( $index ); ?>][row_id]" value="<?php echo esc_attr( $item_id ); ?>" <?php disabled( $parent_disabled ); ?> /> | |
| 1376 | + <button type="button" class="repeater-item-header" aria-expanded="false" aria-controls="<?php echo esc_attr( $content_id ); ?>"> | |
| 1298 | 1377 | <?php |
| 1299 | 1378 | $display_field = ! empty( $args['live_update_field'] ) ? $args['live_update_field'] : 'name'; |
| 1300 | 1379 | $live_options = ! empty( $args['live_update_field_options'] ) && is_array( $args['live_update_field_options'] ) ? $args['live_update_field_options'] : array(); |
| 1301 | 1380 | $raw_live_value = ! empty( $item['fields'][ $display_field ] ) ? (string) $item['fields'][ $display_field ] : ''; |
| @@ -1303,11 +1382,11 @@ | ||
| 1303 | 1382 | ? ( isset( $live_options[ $raw_live_value ] ) ? $live_options[ $raw_live_value ] : $raw_live_value ) |
| 1304 | 1383 | : $fallback_title; |
| 1305 | 1384 | ?> |
| 1306 | 1385 | <span class="repeater-title"><?php echo esc_html( $display_value ); ?></span> |
| 1307 | - <span class="toggle-icon">▼</span> | |
| 1308 | - </div> | |
| 1309 | - <div class="repeater-item-content" style="display: none;"> | |
| 1386 | + <span class="toggle-icon" aria-hidden="true">▼</span> | |
| 1387 | + </button> | |
| 1388 | + <div id="<?php echo esc_attr( $content_id ); ?>" class="repeater-item-content" aria-hidden="true" style="display: none;"> | |
| 1310 | 1389 | <?php |
| 1311 | 1390 | foreach ( $args['fields'] as $field ) { |
| 1312 | 1391 | $field_id = sanitize_key( $field['id'] ); |
| 1313 | 1392 | |
| @@ -1318,8 +1397,18 @@ | ||
| 1318 | 1397 | '_repeater_id' => $args['id'], |
| 1319 | 1398 | '_index' => $index, |
| 1320 | 1399 | ) |
| 1321 | 1400 | ); |
| 1401 | + | |
| 1402 | + // Subfields do not inherit the repeater's own arguments. | |
| 1403 | + if ( $parent_disabled ) { | |
| 1404 | + $field_args['disabled'] = true; | |
| 1405 | + } | |
| 1406 | + | |
| 1407 | + // Rows collapse, and a browser blocks submit on a hidden required | |
| 1408 | + // control without reporting why. The label keeps its asterisk. | |
| 1409 | + $field_args['required'] = false; | |
| 1410 | + | |
| 1322 | 1411 | $field_args = Settings_API::parse_field_args( $field_args, $args['section'] ); |
| 1323 | 1412 | |
| 1324 | 1413 | if ( ! isset( $field['type'] ) || ! is_string( $field['type'] ) ) { |
| 1325 | 1414 | continue; |
| @@ -1352,16 +1441,16 @@ | ||
| 1352 | 1441 | </div> |
| 1353 | 1442 | |
| 1354 | 1443 | <div class="repeater-item-footer"> |
| 1355 | 1444 | <div class="repeater-item-actions"> |
| 1356 | - <button type="button" class="button button-secondary move-up"> | |
| 1357 | - <span class="dashicons dashicons-arrow-up-alt2"></span> | |
| 1445 | + <button type="button" class="button button-secondary move-up" aria-label="<?php echo esc_attr( $this->translation_strings['repeater_move_up'] ?? 'Move item up' ); ?>" <?php disabled( $parent_disabled ); ?>> | |
| 1446 | + <span class="dashicons dashicons-arrow-up-alt2" aria-hidden="true"></span> | |
| 1358 | 1447 | </button> |
| 1359 | - <button type="button" class="button button-secondary move-down"> | |
| 1360 | - <span class="dashicons dashicons-arrow-down-alt2"></span> | |
| 1448 | + <button type="button" class="button button-secondary move-down" aria-label="<?php echo esc_attr( $this->translation_strings['repeater_move_down'] ?? 'Move item down' ); ?>" <?php disabled( $parent_disabled ); ?>> | |
| 1449 | + <span class="dashicons dashicons-arrow-down-alt2" aria-hidden="true"></span> | |
| 1361 | 1450 | </button> |
| 1362 | - <button type="button" class="button button-secondary remove-item"> | |
| 1363 | - <span class="dashicons dashicons-trash"></span> | |
| 1451 | + <button type="button" class="button button-secondary remove-item" aria-label="<?php echo esc_attr( $this->translation_strings['repeater_remove_item'] ?? 'Remove item' ); ?>" <?php disabled( $parent_disabled ); ?>> | |
| 1452 | + <span class="dashicons dashicons-trash" aria-hidden="true"></span> | |
| 1364 | 1453 | </button> |
| 1365 | 1454 | </div> |
| 1366 | 1455 | </div> |
| 1367 | 1456 | </div> |