Ticket #36944: 36944.1.diff
File 36944.1.diff, 21.4 KB (added by , 9 years ago) |
---|
-
src/wp-admin/js/customize-controls.js
diff --git src/wp-admin/js/customize-controls.js src/wp-admin/js/customize-controls.js index 2a5a5b9..1b3f9cd 100644
1543 1543 1544 1544 control.setting = control.settings['default'] || null; 1545 1545 1546 // Add setting notifications to the control notification. 1546 1547 _.each( control.settings, function( setting ) { 1547 1548 setting.notifications.bind( 'add', function( settingNotification ) { 1548 var controlNotification = new api.Notification( setting.id + ':' + settingNotification.code, settingNotification ); 1549 var controlNotification, code, params; 1550 code = setting.id + ':' + settingNotification.code; 1551 params = _.extend( 1552 {}, 1553 settingNotification, 1554 { 1555 setting: setting.id 1556 } 1557 ); 1558 controlNotification = new api.Notification( code, params ); 1549 1559 control.notifications.add( controlNotification.code, controlNotification ); 1550 1560 } ); 1551 1561 setting.notifications.bind( 'remove', function( settingNotification ) { … … 2908 2918 } 2909 2919 } ); 2910 2920 } ); 2921 2922 if ( data.settingValidities ) { 2923 api._handleSettingValidities( { 2924 settingValidities: data.settingValidities, 2925 focusInvalidControl: false 2926 } ); 2927 } 2911 2928 } ); 2912 2929 2913 2930 this.request = $.ajax( this.previewUrl(), { … … 3430 3447 }; 3431 3448 }, 3432 3449 3433 /**3434 * Handle invalid_settings in an error response for the customize-save request.3435 *3436 * Add notifications to the settings and focus on the first control that has an invalid setting.3437 *3438 * @since 4.6.03439 * @private3440 *3441 * @param {object} response3442 * @param {object} response.invalid_settings3443 * @returns {void}3444 */3445 _handleInvalidSettingsError: function( response ) {3446 var invalidControls = [], wasFocused = false;3447 if ( _.isEmpty( response.invalid_settings ) ) {3448 return;3449 }3450 3451 // Find the controls that correspond to each invalid setting.3452 _.each( response.invalid_settings, function( notifications, settingId ) {3453 var setting = api( settingId );3454 if ( setting ) {3455 _.each( notifications, function( notificationParams, code ) {3456 var notification = new api.Notification( code, notificationParams );3457 setting.notifications.add( code, notification );3458 } );3459 }3460 3461 api.control.each( function( control ) {3462 _.each( control.settings, function( controlSetting ) {3463 if ( controlSetting.id === settingId ) {3464 invalidControls.push( control );3465 }3466 } );3467 } );3468 } );3469 3470 // Focus on the first control that is inside of an expanded section (one that is visible).3471 _( invalidControls ).find( function( control ) {3472 var isExpanded = control.section() && api.section.has( control.section() ) && api.section( control.section() ).expanded();3473 if ( isExpanded && control.expanded ) {3474 isExpanded = control.expanded();3475 }3476 if ( isExpanded ) {3477 control.focus();3478 wasFocused = true;3479 }3480 return wasFocused;3481 } );3482 3483 // Focus on the first invalid control.3484 if ( ! wasFocused && invalidControls[0] ) {3485 invalidControls[0].focus();3486 }3487 },3488 3489 3450 save: function() { 3490 3451 var self = this, 3491 3452 processing = api.state( 'processing' ), 3492 3453 submitWhenDoneProcessing, 3493 3454 submit, 3494 modifiedWhileSaving = {}; 3455 modifiedWhileSaving = {}, 3456 invalidSettings = [], 3457 invalidControls; 3495 3458 3496 3459 body.addClass( 'saving' ); 3497 3460 … … 3502 3465 3503 3466 submit = function () { 3504 3467 var request, query; 3468 3469 /* 3470 * Block saving if there are any settings that are marked as 3471 * invalid from the client (not from the server). Focus on 3472 * the control. 3473 */ 3474 api.each( function( setting ) { 3475 setting.notifications.each( function( notification ) { 3476 if ( 'error' === notification.type && ( ! notification.data || ! notification.data.from_server ) ) { 3477 invalidSettings.push( setting.id ); 3478 } 3479 } ); 3480 } ); 3481 invalidControls = api.findControlsForSettings( invalidSettings ); 3482 if ( ! _.isEmpty( invalidControls ) ) { 3483 _.values( invalidControls )[0][0].focus(); 3484 body.removeClass( 'saving' ); 3485 api.unbind( 'change', captureSettingModifiedDuringSave ); 3486 return; 3487 } 3488 3505 3489 query = $.extend( self.query(), { 3506 3490 nonce: self.nonce.save 3507 3491 } ); … … 3512 3496 3513 3497 api.trigger( 'save', request ); 3514 3498 3515 /*3516 * Remove all setting error notifications prior to save, allowing3517 * server to respond with fresh validation error notifications.3518 */3519 api.each( function( setting ) {3520 setting.notifications.each( function( notification ) {3521 if ( 'error' === notification.type ) {3522 setting.notifications.remove( notification.code );3523 }3524 } );3525 } );3526 3527 3499 request.always( function () { 3528 3500 body.removeClass( 'saving' ); 3529 3501 saveBtn.prop( 'disabled', false ); … … 3548 3520 } ); 3549 3521 } 3550 3522 3551 self._handleInvalidSettingsError( response ); 3523 if ( response.setting_validities ) { 3524 api._handleSettingValidities( { 3525 settingValidities: response.setting_validities, 3526 focusInvalidControl: true 3527 } ); 3528 } 3552 3529 3553 3530 api.trigger( 'error', response ); 3554 3531 } ); … … 3564 3541 3565 3542 api.previewer.send( 'saved', response ); 3566 3543 3544 if ( response.setting_validities ) { 3545 api._handleSettingValidities( { 3546 settingValidities: response.setting_validities, 3547 focusInvalidControl: true 3548 } ); 3549 } 3550 3567 3551 api.trigger( 'saved', response ); 3568 3552 3569 3553 // Restore the global dirty state if any settings were modified during save. … … 3670 3654 }); 3671 3655 3672 3656 /** 3657 * Handle setting_validities in an error response for the customize-save request. 3658 * 3659 * Add notifications to the settings and focus on the first control that has an invalid setting. 3660 * 3661 * @since 4.6.0 3662 * @private 3663 * 3664 * @param {object} args 3665 * @param {object} args.settingValidities 3666 * @param {boolean} [args.focusInvalidControl=false] 3667 * @returns {void} 3668 */ 3669 api._handleSettingValidities = function handleSettingValidities( args ) { 3670 var invalidSettingControls, invalidSettings = [], wasFocused = false; 3671 3672 // Find the controls that correspond to each invalid setting. 3673 _.each( args.settingValidities, function( validity, settingId ) { 3674 var setting = api( settingId ); 3675 if ( setting ) { 3676 3677 // Add notifications for invalidities. 3678 if ( _.isObject( validity ) ) { 3679 _.each( validity, function( params, code ) { 3680 var notification = new api.Notification( code, params ), existingNotification, needsReplacement = false; 3681 3682 // Remove existing notification if already exists for code but differs in parameters. 3683 existingNotification = setting.notifications( notification.code ); 3684 if ( existingNotification ) { 3685 needsReplacement = ( notification.type !== existingNotification.type ) || ! _.isEqual( notification.data, existingNotification.data ); 3686 } 3687 if ( needsReplacement ) { 3688 setting.notifications.remove( code ); 3689 } 3690 3691 if ( ! setting.notifications.has( notification.code ) ) { 3692 setting.notifications.add( code, notification ); 3693 } 3694 invalidSettings.push( setting.id ); 3695 } ); 3696 } 3697 3698 // Remove notification errors that are no longer valid. 3699 setting.notifications.each( function( notification ) { 3700 if ( 'error' === notification.type && ( true === validity || ! validity[ notification.code ] ) ) { 3701 setting.notifications.remove( notification.code ); 3702 } 3703 } ); 3704 } 3705 } ); 3706 3707 if ( args.focusInvalidControl ) { 3708 invalidSettingControls = api.findControlsForSettings( invalidSettings ); 3709 3710 // Focus on the first control that is inside of an expanded section (one that is visible). 3711 _( _.values( invalidSettingControls ) ).find( function( controls ) { 3712 return _( controls ).find( function( control ) { 3713 var isExpanded = control.section() && api.section.has( control.section() ) && api.section( control.section() ).expanded(); 3714 if ( isExpanded && control.expanded ) { 3715 isExpanded = control.expanded(); 3716 } 3717 if ( isExpanded ) { 3718 control.focus(); 3719 wasFocused = true; 3720 } 3721 return wasFocused; 3722 } ); 3723 } ); 3724 3725 // Focus on the first invalid control. 3726 if ( ! wasFocused && ! _.isEmpty( invalidSettingControls ) ) { 3727 _.values( invalidSettingControls )[0][0].focus(); 3728 } 3729 } 3730 }; 3731 3732 /** 3733 * Find all controls associated with the given settings. 3734 * 3735 * @since 4.6.0 3736 * @param {string[]} settingIds Setting IDs. 3737 * @returns {object<string, wp.customize.Control>} Mapping setting ids to arrays of controls. 3738 */ 3739 api.findControlsForSettings = function findControlsForSettings( settingIds ) { 3740 var controls = {}; 3741 _.each( _.unique( settingIds ), function( settingId ) { 3742 api.control.each( function( control ) { 3743 _.each( control.settings, function( controlSetting ) { 3744 if ( controlSetting.id === settingId ) { 3745 if ( ! controls[ settingId ] ) { 3746 controls[ settingId ] = []; 3747 } 3748 controls[ settingId ].push( control ); 3749 } 3750 } ); 3751 } ); 3752 } ); 3753 return controls; 3754 }; 3755 3756 /** 3673 3757 * Sort panels, sections, controls by priorities. Hide empty sections and panels. 3674 3758 * 3675 3759 * @since 4.1.0 … … 4040 4124 }); 4041 4125 }); 4042 4126 4127 // Update the setting validities 4128 api.previewer.bind( 'selective-refresh-setting-validities', function handleSelectiveRefreshedSettingValidities( settingValidities ) { 4129 api._handleSettingValidities( { 4130 settingValidities: settingValidities, 4131 focusInvalidControl: false 4132 } ); 4133 } ); 4134 4043 4135 // Focus on the control that is associated with the given setting. 4044 4136 api.previewer.bind( 'focus-control-for-setting', function( settingId ) { 4045 4137 var matchedControl; -
src/wp-includes/class-wp-customize-manager.php
diff --git src/wp-includes/class-wp-customize-manager.php src/wp-includes/class-wp-customize-manager.php index f89887e..63d9f1d 100644
final class WP_Customize_Manager { 825 825 * @since 3.4.0 826 826 */ 827 827 public function customize_preview_settings() { 828 $setting_validities = $this->validate_setting_values( $this->unsanitized_post_values() ); 829 $exported_setting_validities = array_map( array( $this, 'prepare_setting_validity_for_js' ), $setting_validities ); 830 828 831 $settings = array( 829 832 'theme' => array( 830 833 'stylesheet' => $this->get_stylesheet(), … … final class WP_Customize_Manager { 837 840 'activePanels' => array(), 838 841 'activeSections' => array(), 839 842 'activeControls' => array(), 843 'settingValidities' => $exported_setting_validities, 840 844 'nonce' => $this->get_nonces(), 841 845 'l10n' => array( 842 846 'shiftClickToEdit' => __( 'Shift-click to edit this element.' ), … … final class WP_Customize_Manager { 991 995 * @since 4.6.0 992 996 * @access public 993 997 * @see WP_REST_Request::has_valid_params() 998 * @see WP_Customize_Setting::validate() 994 999 * 995 1000 * @param array $setting_values Mapping of setting IDs to values to sanitize and validate. 996 * @return array Empty array if all settings were valid. One or more instances of `WP_Error` if any were invalid.1001 * @return array Mapping of setting IDs to return value of validate method calls, either `true` or `WP_Error`. 997 1002 */ 998 1003 public function validate_setting_values( $setting_values ) { 999 $validit y_errors = array();1004 $validities = array(); 1000 1005 foreach ( $setting_values as $setting_id => $unsanitized_value ) { 1001 1006 $setting = $this->get_setting( $setting_id ); 1002 1007 if ( ! $setting || is_null( $unsanitized_value ) ) { … … final class WP_Customize_Manager { 1006 1011 if ( false === $validity || null === $validity ) { 1007 1012 $validity = new WP_Error( 'invalid_value', __( 'Invalid value.' ) ); 1008 1013 } 1009 if ( is_wp_error( $validity ) ) { 1010 $validity_errors[ $setting_id ] = $validity; 1014 $validities[ $setting_id ] = $validity; 1015 } 1016 return $validities; 1017 } 1018 1019 /** 1020 * Prepare setting validity for exporting to the client (JS). 1021 * 1022 * Converts `WP_Error` instance into array suitable for passing into the 1023 * `wp.customize.Notification` JS model. 1024 * 1025 * @since 4.6.0 1026 * @access public 1027 * 1028 * @param true|WP_Error $validity Setting validity. 1029 * @return true|array If `$validity` was `WP_Error` then array mapping the error 1030 * codes to their respective `message` and `data` to pass 1031 * into the `wp.customize.Notification` JS model. 1032 */ 1033 public function prepare_setting_validity_for_js( $validity ) { 1034 if ( is_wp_error( $validity ) ) { 1035 $notification = array(); 1036 foreach ( $validity->errors as $error_code => $error_messages ) { 1037 $error_data = $validity->get_error_data( $error_code ); 1038 if ( is_null( $error_data ) ) { 1039 $error_data = array(); 1040 } 1041 $error_data = array_merge( 1042 $error_data, 1043 array( 'from_server' => true ) 1044 ); 1045 $notification[ $error_code ] = array( 1046 'message' => join( ' ', $error_messages ), 1047 'data' => $error_data, 1048 ); 1011 1049 } 1050 return $notification; 1051 } else { 1052 return true; 1012 1053 } 1013 return $validity_errors;1014 1054 } 1015 1055 1016 1056 /** … … final class WP_Customize_Manager { 1041 1081 do_action( 'customize_save_validation_before', $this ); 1042 1082 1043 1083 // Validate settings. 1044 $validity_errors = $this->validate_setting_values( $this->unsanitized_post_values() ); 1045 $invalid_count = count( $validity_errors ); 1046 if ( $invalid_count > 0 ) { 1047 $settings_errors = array(); 1048 foreach ( $validity_errors as $setting_id => $validity_error ) { 1049 $settings_errors[ $setting_id ] = array(); 1050 foreach ( $validity_error->errors as $error_code => $error_messages ) { 1051 $settings_errors[ $setting_id ][ $error_code ] = array( 1052 'message' => join( ' ', $error_messages ), 1053 'data' => $validity_error->get_error_data( $error_code ), 1054 ); 1055 } 1056 } 1084 $setting_validities = $this->validate_setting_values( $this->unsanitized_post_values() ); 1085 $invalid_setting_count = count( array_filter( $setting_validities, 'is_wp_error' ) ); 1086 $exported_setting_validities = array_map( array( $this, 'prepare_setting_validity_for_js' ), $setting_validities ); 1087 if ( $invalid_setting_count > 0 ) { 1057 1088 $response = array( 1058 ' invalid_settings' => $settings_errors,1059 'message' => sprintf( _n( 'There is %s invalid setting.', 'There are %s invalid settings.', $invalid_ count ), number_format_i18n( $invalid_count ) ),1089 'setting_validities' => $exported_setting_validities, 1090 'message' => sprintf( _n( 'There is %s invalid setting.', 'There are %s invalid settings.', $invalid_setting_count ), number_format_i18n( $invalid_setting_count ) ), 1060 1091 ); 1061 1092 1062 1093 /** This filter is documented in wp-includes/class-wp-customize-manager.php */ … … final class WP_Customize_Manager { 1097 1128 */ 1098 1129 do_action( 'customize_save_after', $this ); 1099 1130 1131 $data = array( 1132 'setting_validities' => $exported_setting_validities, 1133 ); 1134 1100 1135 /** 1101 1136 * Filters response data for a successful customize_save AJAX request. 1102 1137 * … … final class WP_Customize_Manager { 1108 1143 * event on `wp.customize`. 1109 1144 * @param WP_Customize_Manager $this WP_Customize_Manager instance. 1110 1145 */ 1111 $response = apply_filters( 'customize_save_response', array(), $this );1146 $response = apply_filters( 'customize_save_response', $data, $this ); 1112 1147 wp_send_json_success( $response ); 1113 1148 } 1114 1149 -
src/wp-includes/customize/class-wp-customize-selective-refresh.php
diff --git src/wp-includes/customize/class-wp-customize-selective-refresh.php src/wp-includes/customize/class-wp-customize-selective-refresh.php index f90f0f9..245d32a 100644
final class WP_Customize_Selective_Refresh { 402 402 $response['errors'] = $this->triggered_errors; 403 403 } 404 404 405 $setting_validities = $this->manager->validate_setting_values( $this->manager->unsanitized_post_values() ); 406 $exported_setting_validities = array_map( array( $this->manager, 'prepare_setting_validity_for_js' ), $setting_validities ); 407 $response['setting_validities'] = $exported_setting_validities; 408 405 409 /** 406 410 * Filters the response from rendering the partials. 407 411 * -
src/wp-includes/js/customize-preview.js
diff --git src/wp-includes/js/customize-preview.js src/wp-includes/js/customize-preview.js index ac77551..f5569ed 100644
172 172 api.preview.send( 'ready', { 173 173 activePanels: api.settings.activePanels, 174 174 activeSections: api.settings.activeSections, 175 activeControls: api.settings.activeControls 175 activeControls: api.settings.activeControls, 176 settingValidities: api.settings.settingValidities 176 177 } ); 177 178 178 179 // Display a loading indicator when preview is reloading, and remove on failure. -
src/wp-includes/js/customize-selective-refresh.js
diff --git src/wp-includes/js/customize-selective-refresh.js src/wp-includes/js/customize-selective-refresh.js index 7efee3d..ec51058 100644
wp.customize.selectiveRefresh = ( function( $, api ) { 847 847 } 848 848 } ); 849 849 850 /** 851 * Handle setting validities in partial refresh response. 852 * 853 * @param {object} data Response data. 854 * @param {object} data.setting_validities Setting validities. 855 */ 856 api.selectiveRefresh.bind( 'render-partials-response', function handleSettingValiditiesResponse( data ) { 857 if ( data.setting_validities ) { 858 api.preview.send( 'selective-refresh-setting-validities', data.setting_validities ); 859 } 860 } ); 861 850 862 api.preview.bind( 'active', function() { 851 863 852 864 // Make all partials ready. -
tests/phpunit/tests/customize/manager.php
diff --git tests/phpunit/tests/customize/manager.php tests/phpunit/tests/customize/manager.php index 666db6f..437f410 100644
class Tests_WP_Customize_Manager extends WP_UnitTestCase { 196 196 * @see WP_Customize_Manager::validate_setting_values() 197 197 */ 198 198 function test_validate_setting_values() { 199 $default_value = 'foo_default';200 199 $setting = $this->manager->add_setting( 'foo', array( 201 200 'validate_callback' => array( $this, 'filter_customize_validate_foo' ), 202 201 'sanitize_callback' => array( $this, 'filter_customize_sanitize_foo' ), … … class Tests_WP_Customize_Manager extends WP_UnitTestCase { 204 203 205 204 $post_value = 'bar'; 206 205 $this->manager->set_post_value( 'foo', $post_value ); 207 $this->assertEmpty( $this->manager->validate_setting_values( $this->manager->unsanitized_post_values() ) ); 206 $validities = $this->manager->validate_setting_values( $this->manager->unsanitized_post_values() ); 207 $this->assertCount( 1, $validities ); 208 $this->assertEquals( array( 'foo' => true ), $validities ); 208 209 209 210 $this->manager->set_post_value( 'foo', 'return_wp_error_in_sanitize' ); 210 211 $invalid_settings = $this->manager->validate_setting_values( $this->manager->unsanitized_post_values() ); … … class Tests_WP_Customize_Manager extends WP_UnitTestCase { 234 235 } 235 236 236 237 /** 238 * Test WP_Customize_Manager::prepare_setting_validity_for_js(). 239 * 240 * @see WP_Customize_Manager::prepare_setting_validity_for_js() 241 */ 242 function test_prepare_setting_validity_for_js() { 243 $this->assertTrue( $this->manager->prepare_setting_validity_for_js( true ) ); 244 $error = new WP_Error(); 245 $error->add( 'bad_letter', 'Bad letter' ); 246 $error->add( 'bad_letter', 'Bad letra' ); 247 $error->add( 'bad_number', 'Bad number', array( 'number' => 123 ) ); 248 $validity = $this->manager->prepare_setting_validity_for_js( $error ); 249 $this->assertInternalType( 'array', $validity ); 250 foreach ( $error->errors as $code => $messages ) { 251 $this->assertArrayHasKey( $code, $validity ); 252 $this->assertInternalType( 'array', $validity[ $code ] ); 253 $this->assertEquals( join( ' ', $messages ), $validity[ $code ]['message'] ); 254 $this->assertArrayHasKey( 'data', $validity[ $code ] ); 255 $this->assertArrayHasKey( 'from_server', $validity[ $code ]['data'] ); 256 } 257 $this->assertArrayHasKey( 'number', $validity['bad_number']['data'] ); 258 $this->assertEquals( 123, $validity['bad_number']['data']['number'] ); 259 } 260 261 /** 237 262 * Test WP_Customize_Manager::set_post_value(). 238 263 * 239 264 * @see WP_Customize_Manager::set_post_value() … … class Tests_WP_Customize_Manager extends WP_UnitTestCase { 565 590 $this->assertArrayHasKey( 'activePanels', $settings ); 566 591 $this->assertArrayHasKey( 'activeSections', $settings ); 567 592 $this->assertArrayHasKey( 'activeControls', $settings ); 593 $this->assertArrayHasKey( 'settingValidities', $settings ); 568 594 $this->assertArrayHasKey( 'nonce', $settings ); 569 595 $this->assertArrayHasKey( '_dirty', $settings ); 570 596 -
tests/phpunit/tests/customize/selective-refresh-ajax.php
diff --git tests/phpunit/tests/customize/selective-refresh-ajax.php tests/phpunit/tests/customize/selective-refresh-ajax.php index b60f23a..d2a9cdf 100644
class Test_WP_Customize_Selective_Refresh_Ajax extends WP_UnitTestCase { 344 344 $this->assertEquals( $count_customize_render_partials_after + 1, has_action( 'customize_render_partials_after' ) ); 345 345 $output = json_decode( ob_get_clean(), true ); 346 346 $this->assertEquals( array( get_bloginfo( 'name', 'display' ) ), $output['data']['contents']['test_blogname'] ); 347 $this->assertArrayHasKey( 'setting_validities', $output['data'] ); 347 348 } 348 349 349 350 /**