Ticket #34893: 34893.6.diff
File 34893.6.diff, 19.8 KB (added by , 9 years ago) |
---|
-
wp-admin/css/customize-controls.css
494 494 .customize-control input[type="search"], 495 495 .customize-control input[type="tel"], 496 496 .customize-control input[type="url"] { 497 width: 98%;497 width: 100%; 498 498 line-height: 18px; 499 499 margin: 0; 500 500 } … … 622 622 border-right: 1px solid #ddd; 623 623 } 624 624 625 626 /** 627 * Notifications 628 */ 629 630 #customize-controls .customize-control-notifications-container { /* Scoped to #customize-controls for specificity over notification styles in common.css. */ 631 margin: 4px 0 8px 0; 632 padding: 5px 10px; 633 display: none; 634 cursor: default; 635 border-top: 1px solid #eee; 636 border-bottom: 1px solid #eee; 637 border-right: 1px solid #eee; 638 } 639 640 .customize-control-widget_form.customize-control-has-notifications .widget .widget-top, 641 .customize-control-nav_menu_item.customize-control-has-notifications .menu-item-bar .menu-item-handle { 642 background-color: #ffe0e0; 643 } 644 645 .customize-control-notifications-container li { 646 list-style: disc; 647 margin-bottom: 0; 648 margin-left: 1em; 649 } 650 651 .customize-control-notifications-container li:only-child { 652 list-style: none; 653 margin-left: 0; 654 } 655 656 625 657 /* Style for custom settings */ 626 658 627 659 /** -
wp-admin/js/customize-controls.js
27 27 this.id = id; 28 28 this.transport = this.transport || 'refresh'; 29 29 this._dirty = options.dirty || false; 30 this.notifications = new api.Values({ defaultConstructor: api.Notification }); 30 31 31 32 // Whenever the setting's value changes, refresh the preview. 32 33 this.bind( this.preview ); … … 1478 1479 control.priority = new api.Value(); 1479 1480 control.active = new api.Value(); 1480 1481 control.activeArgumentsQueue = []; 1482 control.notifications = new api.Values({ defaultConstructor: api.Notification }); 1481 1483 1482 1484 control.elements = []; 1483 1485 … … 1541 1543 1542 1544 control.setting = control.settings['default'] || null; 1543 1545 1546 _.each( control.settings, function( setting ) { 1547 setting.notifications.bind( 'add', function( settingNotification ) { 1548 var controlNotification = new api.Notification( setting.id + ':' + settingNotification.code, settingNotification ); 1549 control.notifications.add( controlNotification.code, controlNotification ); 1550 } ); 1551 setting.notifications.bind( 'remove', function( settingNotification ) { 1552 control.notifications.remove( setting.id + ':' + settingNotification.code ); 1553 } ); 1554 } ); 1555 1544 1556 control.embed(); 1545 1557 }) ); 1546 1558 } … … 1547 1559 1548 1560 // After the control is embedded on the page, invoke the "ready" method. 1549 1561 control.deferred.embedded.done( function () { 1562 /* 1563 * Note that this debounced/deferred rendering is needed for two reasons: 1564 * 1) The 'remove' event is triggered just _before_ the notification is actually removed. 1565 * 2) Improve performance when adding/removing multiple notifications at a time. 1566 */ 1567 var debouncedRenderNotifications = _.debounce( function renderNotifications() { 1568 control.renderNotifications(); 1569 } ); 1570 control.notifications.bind( 'add', debouncedRenderNotifications ); 1571 control.notifications.bind( 'remove', debouncedRenderNotifications ); 1572 control.renderNotifications(); 1573 1550 1574 control.ready(); 1551 1575 }); 1552 1576 }, … … 1589 1613 ready: function() {}, 1590 1614 1591 1615 /** 1616 * Get the element inside of a control's container that contains the validation error message. 1617 * 1618 * Control subclasses may override this to return the proper container to render notifications into. 1619 * Injects the notification container for existing controls that lack the necessary container, 1620 * including special handling for nav menu items and widgets. 1621 * 1622 * @returns {jQuery} Setting validation message element. 1623 * @this {wp.customize.Control} 1624 */ 1625 getNotificationsContainerElement: function() { 1626 var control = this, controlTitle, notificationsContainer; 1627 1628 notificationsContainer = control.container.find( '.customize-control-notifications-container:first' ); 1629 if ( notificationsContainer.length ) { 1630 return notificationsContainer; 1631 } 1632 1633 notificationsContainer = $( '<div class="customize-control-notifications-container error" aria-live="assertive"></div>' ); 1634 1635 if ( control.container.hasClass( 'customize-control-nav_menu_item' ) ) { 1636 control.container.find( '.menu-item-settings:first' ).prepend( notificationsContainer ); 1637 } else if ( control.container.hasClass( 'customize-control-widget_form' ) ) { 1638 control.container.find( '.widget-inside:first' ).prepend( notificationsContainer ); 1639 } else { 1640 controlTitle = control.container.find( '.customize-control-title' ); 1641 if ( controlTitle.length ) { 1642 controlTitle.after( notificationsContainer ); 1643 } else { 1644 control.container.prepend( notificationsContainer ); 1645 } 1646 } 1647 return notificationsContainer; 1648 }, 1649 1650 /** 1651 * Render notifications. 1652 * 1653 * Renders the `control.notifications` into the control's container. 1654 * Control subclasses may override this method to do their own handling 1655 * of rendering notifications. 1656 */ 1657 renderNotifications: function() { 1658 var control = this, container, notifications; 1659 container = control.getNotificationsContainerElement(); 1660 if ( ! container || ! container.length ) { 1661 return; 1662 } 1663 notifications = []; 1664 control.notifications.each( function( notification ) { 1665 notifications.push( notification ); 1666 } ); 1667 1668 if ( 0 === notifications.length ) { 1669 container.stop().slideUp( 'fast' ); 1670 } else { 1671 container.stop().slideDown( 'fast', null, function() { 1672 $( this ).css( 'height', 'auto' ); 1673 } ); 1674 } 1675 1676 if ( ! control.notificationsTemplate ) { 1677 control.notificationsTemplate = wp.template( 'customize-control-notifications' ); 1678 } 1679 1680 control.container.toggleClass( 'customize-control-has-notifications', 0 !== notifications.length ); 1681 container.empty().append( $.trim( 1682 control.notificationsTemplate( { notifications: notifications } ) 1683 ) ); 1684 }, 1685 1686 /** 1592 1687 * Normal controls do not expand, so just expand its parent 1593 1688 * 1594 1689 * @param {Object} [params] … … 3223 3318 } 3224 3319 }); 3225 3320 3321 api.settingConstructor = {}; 3226 3322 api.controlConstructor = { 3227 3323 color: api.ColorControl, 3228 3324 media: api.MediaControl, … … 3323 3419 }; 3324 3420 }, 3325 3421 3422 _handleInvalidSettingsError: function( response ) { 3423 var invalidControls = [], wasFocused = false; 3424 if ( ! response.invalid_settings || 0 === response.invalid_settings.length ) { 3425 return; 3426 } 3427 3428 // Find the controls that correspond to each invalid setting. 3429 _.each( response.invalid_settings, function( notifications, settingId ) { 3430 var setting = api( settingId ); 3431 if ( setting ) { 3432 _.each( notifications, function( notificationParams, code ) { 3433 var notification = new api.Notification( code, notificationParams ); 3434 setting.notifications.add( code, notification ); 3435 } ); 3436 } 3437 3438 api.control.each( function( control ) { 3439 _.each( control.settings, function( controlSetting ) { 3440 if ( controlSetting.id === settingId ) { 3441 invalidControls.push( control ); 3442 } 3443 } ); 3444 } ); 3445 } ); 3446 3447 // Focus on the first control that is inside of an expanded section (one that is visible). 3448 _( invalidControls ).find( function( control ) { 3449 var isExpanded = control.section() && api.section.has( control.section() ) && api.section( control.section() ).expanded(); 3450 if ( isExpanded && control.expanded ) { 3451 isExpanded = control.expanded(); 3452 } 3453 if ( isExpanded ) { 3454 control.focus(); 3455 wasFocused = true; 3456 } 3457 return wasFocused; 3458 } ); 3459 3460 // Focus on the first invalid control. 3461 if ( ! wasFocused && invalidControls[0] ) { 3462 invalidControls[0].focus(); 3463 } 3464 }, 3465 3326 3466 save: function() { 3327 3467 var self = this, 3328 3468 processing = api.state( 'processing' ), … … 3349 3489 3350 3490 api.trigger( 'save', request ); 3351 3491 3492 // Remove all setting notifications prior to save, allowing server to respond with new notifications. 3493 api.each( function( setting ) { 3494 setting.notifications.each( function( notification ) { 3495 setting.notifications.remove( notification.code ); 3496 } ); 3497 } ); 3498 3352 3499 request.always( function () { 3353 3500 body.removeClass( 'saving' ); 3354 3501 saveBtn.prop( 'disabled', false ); … … 3372 3519 self.preview.iframe.show(); 3373 3520 } ); 3374 3521 } 3522 3523 self._handleInvalidSettingsError( response ); 3524 3375 3525 api.trigger( 'error', response ); 3376 3526 } ); 3377 3527 … … 3424 3574 3425 3575 // Create Settings 3426 3576 $.each( api.settings.settings, function( id, data ) { 3427 api.create( id, id, data.value, { 3577 var constructor = api.settingConstructor[ data.type ] || api.Setting, 3578 setting; 3579 3580 setting = new constructor( id, data.value, { 3428 3581 transport: data.transport, 3429 3582 previewer: api.previewer, 3430 3583 dirty: !! data.dirty 3431 3584 } ); 3585 api.add( id, setting ); 3432 3586 }); 3433 3587 3434 3588 // Create Panels -
wp-includes/class-wp-customize-manager.php
654 654 * Return the sanitized value for a given setting from the request's POST data. 655 655 * 656 656 * @since 3.4.0 657 * @since 4.1.1 Introduced 'default' parameter. 657 * @since 4.1.1 Introduced `$default` parameter. 658 * @since 4.6.0 Return `$default` when setting post value is invalid. 659 * @see WP_REST_Server::dispatch() 660 * @see WP_Rest_Request::sanitize_params() 661 * @see WP_Rest_Request::has_valid_params() 658 662 * 659 * @param WP_Customize_Setting $setting A WP_Customize_Setting derived object 660 * @param mixed $default value returned $setting has no post value (added in 4.2.0). 661 * @return string|mixed $post_value Sanitized value or the $default provided 663 * @param WP_Customize_Setting $setting A WP_Customize_Setting derived object. 664 * @param mixed $default Value returned $setting has no post value (added in 4.2.0) 665 * or the post value is invalid (added in 4.6.0). 666 * @return string|mixed $post_value Sanitized value or the $default provided. 662 667 */ 663 668 public function post_value( $setting, $default = null ) { 664 669 $post_values = $this->unsanitized_post_values(); 665 if ( array_key_exists( $setting->id, $post_values ) ) { 666 return $setting->sanitize( $post_values[ $setting->id ] ); 667 } else { 670 if ( ! array_key_exists( $setting->id, $post_values ) ) { 668 671 return $default; 669 672 } 673 $value = $setting->sanitize( $post_values[ $setting->id ] ); 674 if ( is_null( $value ) || is_wp_error( $value ) ) { 675 return $default; 676 } 677 $valid = $setting->validate( $value ); 678 if ( is_wp_error( $valid ) ) { 679 return $default; 680 } 681 return $value; 670 682 } 671 683 672 684 /** … … 970 982 } 971 983 972 984 /** 985 * Validate setting values. 986 * 987 * Sanitization is applied to the values before being passed for validation. 988 * Validation is skipped for unregistered settings or for values that are 989 * already null since they will be skipped anyway. 990 * 991 * @since 4.6.0 992 * @access public 993 * @see WP_REST_Request::has_valid_params() 994 * 995 * @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. 997 */ 998 public function validate_setting_values( $setting_values ) { 999 $validity_errors = array(); 1000 foreach ( $setting_values as $setting_id => $unsanitized_value ) { 1001 $setting = $this->get_setting( $setting_id ); 1002 if ( ! $setting || is_null( $unsanitized_value ) ) { 1003 continue; 1004 } 1005 $validity = $setting->validate( $setting->sanitize( $unsanitized_value ) ); 1006 if ( false === $validity || null === $validity ) { 1007 $validity = new WP_Error( 'invalid_value', __( 'Invalid value.' ) ); 1008 } 1009 if ( is_wp_error( $validity ) ) { 1010 $validity_errors[ $setting_id ] = $validity; 1011 } 1012 } 1013 return $validity_errors; 1014 } 1015 1016 /** 973 1017 * Switch the theme and trigger the save() method on each setting. 974 1018 * 975 1019 * @since 3.4.0 … … 984 1028 wp_send_json_error( 'invalid_nonce' ); 985 1029 } 986 1030 1031 /** 1032 * Fires before save validation happens. 1033 * 1034 * Plugins can add just-in-time `customize_validate_{$setting_id}` filters 1035 * at this point to catch any settings registered after `customize_register`. 1036 * 1037 * @since 4.6.0 1038 * 1039 * @param WP_Customize_Manager $this WP_Customize_Manager instance. 1040 */ 1041 do_action( 'customize_save_validation_before', $this ); 1042 1043 // 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_message ) { 1051 $settings_errors[ $setting_id ][ $error_code ] = array( 1052 'message' => join( ' ', $error_message ), 1053 'data' => $validity_error->get_error_data( $error_code ), 1054 ); 1055 } 1056 } 1057 $response = array( 1058 'invalid_settings' => $settings_errors, 1059 'message' => sprintf( _n( 'There is %d invalid setting.', 'There are %d invalid settings.', $invalid_count ), $invalid_count ), 1060 ); 1061 1062 /** This filter is documented in wp-includes/class-wp-customize-manager.php */ 1063 $response = apply_filters( 'customize_save_response', $response, $this ); 1064 wp_send_json_error( $response ); 1065 } 1066 987 1067 // Do we have to switch themes? 988 1068 if ( ! $this->is_theme_active() ) { 989 1069 // Temporarily stop previewing the theme to allow switch_themes() … … 1403 1483 ) ); 1404 1484 $control->print_template(); 1405 1485 } 1486 ?> 1487 <script type="text/html" id="tmpl-customize-control-notifications"> 1488 <ul> 1489 <# _.each( data.notifications, function( notification ) { #> 1490 <li class="notification-code-{{ notification.code }}">{{ notification.message || notification.code }}</li> 1491 <# } ); #> 1492 </ul> 1493 </script> 1494 <?php 1406 1495 } 1407 1496 1408 1497 /** … … 1763 1852 printf( 1764 1853 "s[%s] = %s;\n", 1765 1854 wp_json_encode( $setting->id ), 1766 wp_json_encode( array( 1767 'value' => $setting->js_value(), 1768 'transport' => $setting->transport, 1769 'dirty' => $setting->dirty, 1770 ) ) 1855 wp_json_encode( $setting->json() ) 1771 1856 ); 1772 1857 } 1773 1858 } -
wp-includes/class-wp-customize-setting.php
59 59 * 60 60 * @var callback 61 61 */ 62 public $validate_callback = ''; 62 63 public $sanitize_callback = ''; 63 64 public $sanitize_js_callback = ''; 64 65 … … 142 143 $this->id .= '[' . implode( '][', $this->id_data['keys'] ) . ']'; 143 144 } 144 145 146 if ( $this->validate_callback ) { 147 add_filter( "customize_validate_{$this->id}", $this->validate_callback, 10, 3 ); 148 } 145 149 if ( $this->sanitize_callback ) { 146 add_filter( "customize_sanitize_{$this->id}", $this->sanitize_callback, 10, 2);150 add_filter( "customize_sanitize_{$this->id}", $this->sanitize_callback, 10, 3 ); 147 151 } 148 152 if ( $this->sanitize_js_callback ) { 149 153 add_filter( "customize_sanitize_js_{$this->id}", $this->sanitize_js_callback, 10, 2 ); … … 464 468 * the value of the setting. 465 469 * 466 470 * @since 3.4.0 471 * @since 4.6.0 Return the result of updating the value. 467 472 * 468 * @return false|void False if cap check fails or value isn't set .473 * @return false|void False if cap check fails or value isn't set or is invalid. 469 474 */ 470 475 final public function save() { 471 476 $value = $this->post_value(); 472 477 473 if ( ! $this->check_capabilities() || ! isset( $value ) ) 478 if ( ! $this->check_capabilities() || ! isset( $value ) ) { 474 479 return false; 480 } 475 481 476 482 /** 477 483 * Fires when the WP_Customize_Setting::save() method is called. … … 483 489 * 484 490 * @param WP_Customize_Setting $this WP_Customize_Setting instance. 485 491 */ 486 do_action( 'customize_save_' . $this->id_data[ 'base'], $this );492 do_action( 'customize_save_' . $this->id_data['base'], $this ); 487 493 488 494 $this->update( $value ); 489 495 } … … 494 500 * @since 3.4.0 495 501 * 496 502 * @param mixed $default A default value which is used as a fallback. Default is null. 497 * @return mixed The default value on failure, otherwise the sanitized value.503 * @return mixed The default value on failure, otherwise the sanitized and validated value. 498 504 */ 499 505 final public function post_value( $default = null ) { 500 506 return $this->manager->post_value( $this, $default ); … … 505 511 * 506 512 * @since 3.4.0 507 513 * 508 * @param string|array $value The value to sanitize.509 * @return string|array|null Null if an input isn't valid, otherwise the sanitized value.514 * @param string|array $value The value to sanitize. 515 * @return string|array|null|WP_Error Sanitized value, or `null`/`WP_Error` if invalid. 510 516 */ 511 517 public function sanitize( $value ) { 512 518 … … 522 528 } 523 529 524 530 /** 531 * Validate an input. 532 * 533 * @since 4.6.0 534 * @access public 535 * @see WP_REST_Request::has_valid_params() 536 * 537 * @param mixed $value Value to validate. 538 * @return true|WP_Error 539 */ 540 public function validate( $value ) { 541 if ( is_wp_error( $value ) ) { 542 return $value; 543 } 544 if ( is_null( $value ) ) { 545 return new WP_Error( 'invalid_value', __( 'Invalid value.' ) ); 546 } 547 548 $validity = new WP_Error(); 549 550 /** 551 * Validate a Customize setting value. 552 * 553 * Plugins should amend the `$validity` object via its `WP_Error::add()` method. 554 * 555 * @since 4.6.0 556 * 557 * @param WP_Error $validity Filtered from `true` to `WP_Error` when invalid. 558 * @param mixed $value Value of the setting. 559 * @param WP_Customize_Setting $this WP_Customize_Setting instance. 560 */ 561 $validity = apply_filters( "customize_validate_{$this->id}", $validity, $value, $this ); 562 563 if ( is_wp_error( $validity ) && empty( $validity->errors ) ) { 564 $validity = true; 565 } 566 return $validity; 567 } 568 569 /** 525 570 * Get the root value for a setting, especially for multidimensional ones. 526 571 * 527 572 * @since 4.4.0 … … 700 745 } 701 746 702 747 /** 748 * Get the data to export to the client via JSON. 749 * 750 * @since 4.6.0 751 * 752 * @return array Array of parameters passed to JavaScript. 753 */ 754 public function json() { 755 return array( 756 'value' => $this->js_value(), 757 'transport' => $this->transport, 758 'dirty' => $this->dirty, 759 'type' => $this->type, 760 ); 761 } 762 763 /** 703 764 * Validate user capabilities whether the theme supports the setting. 704 765 * 705 766 * @since 3.4.0 -
wp-includes/js/customize-base.js
755 755 // Add the Events mixin to api.Messenger. 756 756 $.extend( api.Messenger.prototype, api.Events ); 757 757 758 /** 759 * Notification. 760 * 761 * @class 762 * @augments wp.customize.Class 763 * 764 * @param {string} code The error code. 765 * @param {object} params Params. 766 * @param {string} params.message The error message. 767 * @param {string} [params.type] The notification type. 768 * @param {*} [params.data] Any additional data. 769 */ 770 api.Notification = api.Class.extend({ 771 initialize: function( code, params ) { 772 this.code = code; 773 this.message = params.message; 774 this.type = params.type || null; 775 this.data = params.data || null; 776 } 777 }); 778 758 779 // The main API object is also a collection of all customizer settings. 759 780 api = $.extend( new api.Values(), api ); 760 781