WordPress.org

Make WordPress Core

Ticket #34893: 34893.12.diff

File 34893.12.diff, 33.6 KB (added by westonruter, 3 years ago)

https://github.com/xwp/wordpress-develop/pull/136/commits/f86adb313cbcfa6817d62b0f270b4d0517f0af79

  • src/wp-admin/css/customize-controls.css

    diff --git src/wp-admin/css/customize-controls.css src/wp-admin/css/customize-controls.css
    index 0a62611..b1fa9ab 100644
    p.customize-section-description { 
    494494.customize-control input[type="search"],
    495495.customize-control input[type="tel"],
    496496.customize-control input[type="url"] {
    497         width: 98%;
     497        width: 100%;
    498498        line-height: 18px;
    499499        margin: 0;
    500500}
    p.customize-section-description { 
    622622        border-right: 1px solid #ddd;
    623623}
    624624
     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: 0;
     633        display: none;
     634        cursor: default;
     635}
     636
     637#customize-controls .customize-control-widget_form.has-error .widget .widget-top,
     638.customize-control-nav_menu_item.has-error .menu-item-bar .menu-item-handle {
     639        border-left: 4px solid #dc3232;
     640        transition: .15s border-left linear;
     641}
     642
     643.customize-control-notifications-container li.notice {
     644        list-style: none;
     645        margin: 0 0 6px 0;
     646        padding: 4px 8px;
     647}
     648
     649.customize-control-notifications-container li.notice:last-child {
     650        margin-bottom: 0;
     651}
     652
     653#customize-controls .customize-control-nav_menu_item .customize-control-notifications-container {
     654        margin-top: 0;
     655}
     656
     657#customize-controls .customize-control-widget_form .customize-control-notifications-container {
     658        margin-top: 8px;
     659}
     660
     661
    625662/* Style for custom settings */
    626663
    627664/**
  • src/wp-admin/js/customize-controls.js

    diff --git src/wp-admin/js/customize-controls.js src/wp-admin/js/customize-controls.js
    index ef01674..2a5a5b9 100644
     
    2727                        this.id = id;
    2828                        this.transport = this.transport || 'refresh';
    2929                        this._dirty = options.dirty || false;
     30                        this.notifications = new api.Values({ defaultConstructor: api.Notification });
    3031
    3132                        // Whenever the setting's value changes, refresh the preview.
    3233                        this.bind( this.preview );
     
    14781479                        control.priority = new api.Value();
    14791480                        control.active = new api.Value();
    14801481                        control.activeArgumentsQueue = [];
     1482                        control.notifications = new api.Values({ defaultConstructor: api.Notification });
    14811483
    14821484                        control.elements = [];
    14831485
     
    15411543
    15421544                                        control.setting = control.settings['default'] || null;
    15431545
     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
    15441556                                        control.embed();
    15451557                                }) );
    15461558                        }
    15471559
    15481560                        // After the control is embedded on the page, invoke the "ready" method.
    15491561                        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', function( notification ) {
     1571                                        wp.a11y.speak( notification.message, 'assertive' );
     1572                                        debouncedRenderNotifications();
     1573                                } );
     1574                                control.notifications.bind( 'remove', debouncedRenderNotifications );
     1575                                control.renderNotifications();
     1576
    15501577                                control.ready();
    15511578                        });
    15521579                },
     
    15891616                ready: function() {},
    15901617
    15911618                /**
     1619                 * Get the element inside of a control's container that contains the validation error message.
     1620                 *
     1621                 * Control subclasses may override this to return the proper container to render notifications into.
     1622                 * Injects the notification container for existing controls that lack the necessary container,
     1623                 * including special handling for nav menu items and widgets.
     1624                 *
     1625                 * @since 4.6.0
     1626                 * @returns {jQuery} Setting validation message element.
     1627                 * @this {wp.customize.Control}
     1628                 */
     1629                getNotificationsContainerElement: function() {
     1630                        var control = this, controlTitle, notificationsContainer;
     1631
     1632                        notificationsContainer = control.container.find( '.customize-control-notifications-container:first' );
     1633                        if ( notificationsContainer.length ) {
     1634                                return notificationsContainer;
     1635                        }
     1636
     1637                        notificationsContainer = $( '<div class="customize-control-notifications-container"></div>' );
     1638
     1639                        if ( control.container.hasClass( 'customize-control-nav_menu_item' ) ) {
     1640                                control.container.find( '.menu-item-settings:first' ).prepend( notificationsContainer );
     1641                        } else if ( control.container.hasClass( 'customize-control-widget_form' ) ) {
     1642                                control.container.find( '.widget-inside:first' ).prepend( notificationsContainer );
     1643                        } else {
     1644                                controlTitle = control.container.find( '.customize-control-title' );
     1645                                if ( controlTitle.length ) {
     1646                                        controlTitle.after( notificationsContainer );
     1647                                } else {
     1648                                        control.container.prepend( notificationsContainer );
     1649                                }
     1650                        }
     1651                        return notificationsContainer;
     1652                },
     1653
     1654                /**
     1655                 * Render notifications.
     1656                 *
     1657                 * Renders the `control.notifications` into the control's container.
     1658                 * Control subclasses may override this method to do their own handling
     1659                 * of rendering notifications.
     1660                 *
     1661                 * @since 4.6.0
     1662                 * @this {wp.customize.Control}
     1663                 */
     1664                renderNotifications: function() {
     1665                        var control = this, container, notifications, hasError = false;
     1666                        container = control.getNotificationsContainerElement();
     1667                        if ( ! container || ! container.length ) {
     1668                                return;
     1669                        }
     1670                        notifications = [];
     1671                        control.notifications.each( function( notification ) {
     1672                                notifications.push( notification );
     1673                                if ( 'error' === notification.type ) {
     1674                                        hasError = true;
     1675                                }
     1676                        } );
     1677
     1678                        if ( 0 === notifications.length ) {
     1679                                container.stop().slideUp( 'fast' );
     1680                        } else {
     1681                                container.stop().slideDown( 'fast', null, function() {
     1682                                        $( this ).css( 'height', 'auto' );
     1683                                } );
     1684                        }
     1685
     1686                        if ( ! control.notificationsTemplate ) {
     1687                                control.notificationsTemplate = wp.template( 'customize-control-notifications' );
     1688                        }
     1689
     1690                        control.container.toggleClass( 'has-notifications', 0 !== notifications.length );
     1691                        control.container.toggleClass( 'has-error', hasError );
     1692                        container.empty().append( $.trim(
     1693                                control.notificationsTemplate( { notifications: notifications, altNotice: Boolean( control.altNotice ) } )
     1694                        ) );
     1695                },
     1696
     1697                /**
    15921698                 * Normal controls do not expand, so just expand its parent
    15931699                 *
    15941700                 * @param {Object} [params]
     
    32233329                }
    32243330        });
    32253331
     3332        api.settingConstructor = {};
    32263333        api.controlConstructor = {
    32273334                color:         api.ColorControl,
    32283335                media:         api.MediaControl,
     
    33233430                                };
    33243431                        },
    33253432
     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.0
     3439                         * @private
     3440                         *
     3441                         * @param {object} response
     3442                         * @param {object} response.invalid_settings
     3443                         * @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
    33263489                        save: function() {
    33273490                                var self = this,
    33283491                                        processing = api.state( 'processing' ),
     
    33493512
    33503513                                        api.trigger( 'save', request );
    33513514
     3515                                        /*
     3516                                         * Remove all setting error notifications prior to save, allowing
     3517                                         * 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
    33523527                                        request.always( function () {
    33533528                                                body.removeClass( 'saving' );
    33543529                                                saveBtn.prop( 'disabled', false );
     
    33723547                                                                self.preview.iframe.show();
    33733548                                                        } );
    33743549                                                }
     3550
     3551                                                self._handleInvalidSettingsError( response );
     3552
    33753553                                                api.trigger( 'error', response );
    33763554                                        } );
    33773555
     
    34243602
    34253603                // Create Settings
    34263604                $.each( api.settings.settings, function( id, data ) {
    3427                         api.create( id, id, data.value, {
     3605                        var constructor = api.settingConstructor[ data.type ] || api.Setting,
     3606                                setting;
     3607
     3608                        setting = new constructor( id, data.value, {
    34283609                                transport: data.transport,
    34293610                                previewer: api.previewer,
    34303611                                dirty: !! data.dirty
    34313612                        } );
     3613                        api.add( id, setting );
    34323614                });
    34333615
    34343616                // Create Panels
  • src/wp-admin/js/customize-widgets.js

    diff --git src/wp-admin/js/customize-widgets.js src/wp-admin/js/customize-widgets.js
    index aec773b..05028c6 100644
     
    430430                                args = $.extend( {}, control.defaultExpandedArguments, args );
    431431                                control.onChangeExpanded( expanded, args );
    432432                        });
     433                        control.altNotice = true;
    433434
    434435                        api.Control.prototype.initialize.call( control, id, options );
    435436                },
  • 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 0052a1a..911d2e9 100644
    final class WP_Customize_Manager { 
    654654         * Return the sanitized value for a given setting from the request's POST data.
    655655         *
    656656         * @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()
    658662         *
    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.
    662667         */
    663668        public function post_value( $setting, $default = null ) {
    664669                $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 ) ) {
     671                        return $default;
     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 ) ) {
    668679                        return $default;
    669680                }
     681                return $value;
    670682        }
    671683
    672684        /**
    final class WP_Customize_Manager { 
    970982        }
    971983
    972984        /**
     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        /**
    9731017         * Switch the theme and trigger the save() method on each setting.
    9741018         *
    9751019         * @since 3.4.0
    final class WP_Customize_Manager { 
    9841028                        wp_send_json_error( 'invalid_nonce' );
    9851029                }
    9861030
     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_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                        }
     1057                        $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 ) ),
     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
    9871067                // Do we have to switch themes?
    9881068                if ( ! $this->is_theme_active() ) {
    9891069                        // Temporarily stop previewing the theme to allow switch_themes()
    final class WP_Customize_Manager { 
    14031483                        ) );
    14041484                        $control->print_template();
    14051485                }
     1486                ?>
     1487                <script type="text/html" id="tmpl-customize-control-notifications">
     1488                        <ul>
     1489                                <# _.each( data.notifications, function( notification ) { #>
     1490                                        <li class="notice notice-{{ notification.type || 'info' }} {{ data.altNotice ? 'notice-alt' : '' }}" data-code="{{ notification.code }}" data-type="{{ notification.type }}">{{ notification.message || notification.code }}</li>
     1491                                <# } ); #>
     1492                        </ul>
     1493                </script>
     1494                <?php
    14061495        }
    14071496
    14081497        /**
    final class WP_Customize_Manager { 
    17631852                                        printf(
    17641853                                                "s[%s] = %s;\n",
    17651854                                                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() )
    17711856                                        );
    17721857                                }
    17731858                        }
  • src/wp-includes/class-wp-customize-setting.php

    diff --git src/wp-includes/class-wp-customize-setting.php src/wp-includes/class-wp-customize-setting.php
    index 94e7ded..a557b8f 100644
    class WP_Customize_Setting { 
    5959         *
    6060         * @var callback
    6161         */
     62        public $validate_callback    = '';
    6263        public $sanitize_callback    = '';
    6364        public $sanitize_js_callback = '';
    6465
    class WP_Customize_Setting { 
    142143                        $this->id .= '[' . implode( '][', $this->id_data['keys'] ) . ']';
    143144                }
    144145
     146                if ( $this->validate_callback ) {
     147                        add_filter( "customize_validate_{$this->id}", $this->validate_callback, 10, 3 );
     148                }
    145149                if ( $this->sanitize_callback ) {
    146150                        add_filter( "customize_sanitize_{$this->id}", $this->sanitize_callback, 10, 2 );
    147151                }
    class WP_Customize_Setting { 
    464468         * the value of the setting.
    465469         *
    466470         * @since 3.4.0
     471         * @since 4.6.0 Return the result of updating the value.
    467472         *
    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.
    469474         */
    470475        final public function save() {
    471476                $value = $this->post_value();
    472477
    473                 if ( ! $this->check_capabilities() || ! isset( $value ) )
     478                if ( ! $this->check_capabilities() || ! isset( $value ) ) {
    474479                        return false;
     480                }
    475481
    476482                /**
    477483                 * Fires when the WP_Customize_Setting::save() method is called.
    class WP_Customize_Setting { 
    483489                 *
    484490                 * @param WP_Customize_Setting $this WP_Customize_Setting instance.
    485491                 */
    486                 do_action( 'customize_save_' . $this->id_data[ 'base' ], $this );
     492                do_action( 'customize_save_' . $this->id_data['base'], $this );
    487493
    488494                $this->update( $value );
    489495        }
    class WP_Customize_Setting { 
    494500         * @since 3.4.0
    495501         *
    496502         * @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.
    498504         */
    499505        final public function post_value( $default = null ) {
    500506                return $this->manager->post_value( $this, $default );
    class WP_Customize_Setting { 
    505511         *
    506512         * @since 3.4.0
    507513         *
    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.
    510516         */
    511517        public function sanitize( $value ) {
    512518
    class WP_Customize_Setting { 
    522528        }
    523529
    524530        /**
     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        /**
    525570         * Get the root value for a setting, especially for multidimensional ones.
    526571         *
    527572         * @since 4.4.0
    class WP_Customize_Setting { 
    700745        }
    701746
    702747        /**
     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        /**
    703764         * Validate user capabilities whether the theme supports the setting.
    704765         *
    705766         * @since 3.4.0
  • src/wp-includes/js/customize-base.js

    diff --git src/wp-includes/js/customize-base.js src/wp-includes/js/customize-base.js
    index b4b7279..e59f926 100644
    window.wp = window.wp || {}; 
    755755        // Add the Events mixin to api.Messenger.
    756756        $.extend( api.Messenger.prototype, api.Events );
    757757
     758        /**
     759         * Notification.
     760         *
     761         * @class
     762         * @augments wp.customize.Class
     763         * @since 4.6.0
     764         *
     765         * @param {string} code                The error code.
     766         * @param {object} params              Params.
     767         * @param {string} params.message      The error message.
     768         * @param {string} [params.type=error] The notification type.
     769         * @param {*}      [params.data]       Any additional data.
     770         */
     771        api.Notification = api.Class.extend({
     772                initialize: function( code, params ) {
     773                        this.code = code;
     774                        this.message = params.message;
     775                        this.type = params.type || 'error';
     776                        this.data = params.data || null;
     777                }
     778        });
     779
    758780        // The main API object is also a collection of all customizer settings.
    759781        api = $.extend( new api.Values(), api );
    760782
  • tests/phpunit/tests/customize/setting.php

    diff --git tests/phpunit/tests/customize/setting.php tests/phpunit/tests/customize/setting.php
    index c2ff7f5..98990c5 100644
    class Tests_WP_Customize_Setting extends WP_UnitTestCase { 
    4242                $this->assertEquals( 'refresh', $setting->transport );
    4343                $this->assertEquals( '', $setting->sanitize_callback );
    4444                $this->assertEquals( '', $setting->sanitize_js_callback );
     45                $this->assertFalse( has_filter( "customize_validate_{$setting->id}" ) );
    4546                $this->assertFalse( has_filter( "customize_sanitize_{$setting->id}" ) );
    4647                $this->assertFalse( has_filter( "customize_sanitize_js_{$setting->id}" ) );
    4748                $this->assertEquals( false, $setting->dirty );
    class Tests_WP_Customize_Setting extends WP_UnitTestCase { 
    5455                        'theme_supports' => 'widgets',
    5556                        'default' => 'barbar',
    5657                        'transport' => 'postMessage',
     58                        'validate_callback' => create_function( '$value', 'return $value . ":validate_callback";' ),
    5759                        'sanitize_callback' => create_function( '$value', 'return $value . ":sanitize_callback";' ),
    5860                        'sanitize_js_callback' => create_function( '$value', 'return $value . ":sanitize_js_callback";' ),
    5961                );
    class Tests_WP_Customize_Setting extends WP_UnitTestCase { 
    6264                foreach ( $args as $key => $value ) {
    6365                        $this->assertEquals( $value, $setting->$key );
    6466                }
     67                $this->assertEquals( 10, has_filter( "customize_validate_{$setting->id}", $args['validate_callback'] ) );
    6568                $this->assertEquals( 10, has_filter( "customize_sanitize_{$setting->id}", $args['sanitize_callback'] ) );
    6669                $this->assertEquals( 10, has_filter( "customize_sanitize_js_{$setting->id}" ), $args['sanitize_js_callback'] );
    6770        }
    class Tests_WP_Customize_Setting extends WP_UnitTestCase { 
    9093
    9194        /**
    9295         * Run assertions on non-multidimensional standard settings.
     96         *
     97         * @see WP_Customize_Setting::value()
    9398         */
    9499        function test_preview_standard_types_non_multidimensional() {
    95100                $_POST['customized'] = wp_slash( wp_json_encode( $this->post_data_overrides ) );
    class Tests_WP_Customize_Setting extends WP_UnitTestCase { 
    167172         * Run assertions on multidimensional standard settings.
    168173         *
    169174         * @see WP_Customize_Setting::preview()
     175         * @see WP_Customize_Setting::value()
    170176         */
    171177        function test_preview_standard_types_multidimensional() {
    172178                $_POST['customized'] = wp_slash( wp_json_encode( $this->post_data_overrides ) );
    class Tests_WP_Customize_Setting extends WP_UnitTestCase { 
    569575                $autoload = $wpdb->get_var( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name = %s", $id_base ) );
    570576                $this->assertEquals( 'no', $autoload, 'Even though setting1 did not indicate autoload (thus normally true), since another multidimensional option setting of the base did say autoload=false, it should be autoload=no' );
    571577        }
     578
     579        /**
     580         * Test js_value and json methods.
     581         *
     582         * @see WP_Customize_Setting::js_value()
     583         * @see WP_Customize_Setting::json()
     584         */
     585        public function test_js_value() {
     586                $default = "\x00";
     587                $args = array(
     588                        'type' => 'binary',
     589                        'default' => $default,
     590                        'transport' => 'postMessage',
     591                        'dirty' => true,
     592                        'sanitize_js_callback' => create_function( '$value', 'return base64_encode( $value );' ),
     593                );
     594                $setting = new WP_Customize_Setting( $this->manager, 'name', $args );
     595
     596                $this->assertEquals( $default, $setting->value() );
     597                $this->assertEquals( base64_encode( $default ), $setting->js_value() );
     598
     599                $exported = $setting->json();
     600                $this->assertArrayHasKey( 'type', $exported );
     601                $this->assertArrayHasKey( 'value', $exported );
     602                $this->assertArrayHasKey( 'transport', $exported );
     603                $this->assertArrayHasKey( 'dirty', $exported );
     604                $this->assertEquals( $setting->js_value(), $exported['value'] );
     605                $this->assertEquals( $args['type'], $setting->type );
     606                $this->assertEquals( $args['transport'], $setting->transport );
     607                $this->assertEquals( $args['dirty'], $setting->dirty );
     608        }
     609
     610        /**
     611         * Test validate.
     612         *
     613         * @see WP_Customize_Setting::validate()
     614         */
     615        public function test_validate() {
     616                $setting = new WP_Customize_Setting( $this->manager, 'name', array(
     617                        'type' => 'key',
     618                        'validate_callback' => array( $this, 'filter_validate_for_test_validate' ),
     619                ) );
     620                $validity = $setting->validate( 'BAD!' );
     621                $this->assertInstanceOf( 'WP_Error', $validity );
     622                $this->assertEquals( 'invalid_key', $validity->get_error_code() );
     623        }
     624
     625        /**
     626         * Validate callback.
     627         *
     628         * @see Tests_WP_Customize_Setting::test_validate()
     629         *
     630         * @param WP_Error $validity Validity.
     631         * @param string   $value    Value.
     632         *
     633         * @return WP_Error
     634         */
     635        public function filter_validate_for_test_validate( $validity, $value ) {
     636                $this->assertInstanceOf( 'WP_Error', $validity );
     637                $this->assertInternalType( 'string', $value );
     638                if ( sanitize_key( $value ) !== $value ) {
     639                        $validity->add( 'invalid_key', 'Invalid key' );
     640                }
     641                return $validity;
     642        }
    572643}
    573644
  • tests/qunit/fixtures/customize-settings.js

    diff --git tests/qunit/fixtures/customize-settings.js tests/qunit/fixtures/customize-settings.js
    index 85bd1ab..670f783 100644
    window._wpCustomizeSettings = { 
    112112                'fixture-setting': {
    113113                        'transport': 'postMessage',
    114114                        'value': 'Lorem Ipsum'
     115                },
     116                'fixture-setting-abbr': {
     117                        'transport': 'postMessage',
     118                        'value': 'NASA',
     119                        'type': 'abbreviation'
    115120                }
    116121        },
    117122        'theme': {
  • tests/qunit/index.html

    diff --git tests/qunit/index.html tests/qunit/index.html
    index aa454a9..ad8a4eb 100644
     
    1111                <script src="../../src/wp-includes/js/wp-backbone.js"></script>
    1212                <script src="../../src/wp-includes/js/zxcvbn.min.js"></script>
    1313                <script src="../../src/wp-includes/js/wp-util.js"></script>
     14                <script src="../../src/wp-includes/js/wp-a11y.js"></script>
    1415
    1516                <!-- QUnit -->
    1617                <link rel="stylesheet" href="vendor/qunit.css" type="text/css" media="screen" />
     
    126127                                <# } #>
    127128                        </li>
    128129                </script>
     130                <script type="text/html" id="tmpl-customize-control-notifications">
     131                        <ul>
     132                                <# _.each( data.notifications, function( notification ) { #>
     133                                        <li data-code="{{ notification.code }}" data-type="{{ notification.type }}">{{ notification.message || notification.code }}</li>
     134                                <# } ); #>
     135                        </ul>
     136                </script>
    129137
    130138                <!-- Templates for Customizer Menus -->
    131139                <script type="text/html" id="tmpl-customize-control-nav_menu-content">
  • tests/qunit/wp-admin/js/customize-base.js

    diff --git tests/qunit/wp-admin/js/customize-base.js tests/qunit/wp-admin/js/customize-base.js
    index c19ea80..b7aca0d 100644
     
    1 /* global wp */
     1/* global wp, test, ok, equal, module */
    22
    33jQuery( function( $ ) {
    44        var FooSuperClass, BarSubClass, foo, bar, ConstructorTestClass, newConstructor, constructorTest, $mockElement, mockString,
    jQuery( function( $ ) { 
    158158                firstValueInstance.set( 'newValue' );
    159159                ok( wasCallbackFired );
    160160        });
     161
     162        module( 'Customize Base: Notification' );
     163        test( 'Notification object exists and has expected properties', function ( assert ) {
     164                var notification = new wp.customize.Notification( 'mycode', {
     165                        'message': 'Hello World',
     166                        'type': 'update',
     167                        'data': { 'foo': 'bar' }
     168                } );
     169
     170                assert.equal( 'mycode', notification.code );
     171                assert.equal( 'Hello World', notification.message );
     172                assert.equal( 'update', notification.type );
     173                assert.deepEqual( { 'foo': 'bar' }, notification.data );
     174
     175                notification = new wp.customize.Notification( 'mycode2', {
     176                        'message': 'Hello Space'
     177                } );
     178                assert.equal( 'mycode2', notification.code );
     179                assert.equal( 'Hello Space', notification.message );
     180                assert.equal( 'error', notification.type );
     181                assert.equal( null, notification.data );
     182        } );
    161183});
  • tests/qunit/wp-admin/js/customize-controls.js

    diff --git tests/qunit/wp-admin/js/customize-controls.js tests/qunit/wp-admin/js/customize-controls.js
    index 47d695c..23ca9ac 100644
     
    1 /* global wp */
     1/* global wp, test, ok, equal, module */
     2
     3wp.customize.settingConstructor.abbreviation = wp.customize.Setting.extend({
     4        validate: function( value ) {
     5                return value.toUpperCase();
     6        }
     7});
    28
    39jQuery( window ).load( function (){
    410        'use strict';
    jQuery( window ).load( function (){ 
    8591        test( 'Setting has fixture value', function () {
    8692                equal( wp.customize( 'fixture-setting' )(), 'Lorem Ipsum' );
    8793        } );
     94        test( 'Setting has notifications', function () {
     95                var setting = wp.customize( 'fixture-setting' );
     96                ok( setting.notifications.extended( wp.customize.Values ) );
     97                equal( wp.customize.Notification, setting.notifications.prototype.constructor.defaultConstructor );
     98        } );
     99        test( 'Setting constructor object exists', function( assert ) {
     100                assert.ok( _.isObject( wp.customize.settingConstructor ) );
     101        } );
     102        test( 'Custom setting constructor is used', function( assert ) {
     103                var setting = wp.customize( 'fixture-setting-abbr' );
     104                assert.ok( setting.extended( wp.customize.settingConstructor.abbreviation ) );
     105                setting.set( 'usa' );
     106                assert.equal( 'USA', setting.get() );
     107        } );
    88108
    89109        module( 'Customizer Control in Fixture' );
    90110        test( 'Control exists', function () {
    jQuery( window ).load( function (){ 
    99119                var control = wp.customize.control( 'fixture-control' );
    100120                equal( control.section(), 'fixture-section' );
    101121        } );
     122        test( 'Control has notifications', function ( assert ) {
     123                var control = wp.customize.control( 'fixture-control' ), settingNotification, controlOnlyNotification, doneEmbedded;
     124                assert.ok( control.notifications.extended( wp.customize.Values ) );
     125                assert.equal( wp.customize.Notification, control.notifications.prototype.constructor.defaultConstructor );
     126                assert.ok( _.isFunction( control.getNotificationsContainerElement ) );
     127                assert.ok( _.isFunction( control.renderNotifications ) );
     128
     129                doneEmbedded = assert.async();
     130                control.deferred.embedded.done( function() {
     131                        var notificationContainerElement;
     132
     133                        assert.equal( 0, _.size( control.notifications._value ) );
     134                        assert.equal( 0, _.size( control.settings['default'].notifications._value ) );
     135
     136                        notificationContainerElement = control.getNotificationsContainerElement();
     137                        assert.equal( 1, notificationContainerElement.length );
     138                        assert.ok( notificationContainerElement.is( '.customize-control-notifications-container' ) );
     139                        assert.equal( 0, notificationContainerElement.find( '> ul > li' ).length );
     140                        assert.equal( 'none', notificationContainerElement.css( 'display' ) );
     141
     142                        settingNotification = new wp.customize.Notification( 'setting_invalidity', 'Invalid setting' );
     143                        controlOnlyNotification = new wp.customize.Notification( 'control_invalidity', 'Invalid control' );
     144                        control.settings['default'].notifications.add( settingNotification.code, settingNotification );
     145                        control.notifications.add( controlOnlyNotification.code, controlOnlyNotification );
     146
     147                        // Note that renderNotifications is being called manually here since rendering normally happens asynchronously.
     148                        control.renderNotifications();
     149
     150                        assert.equal( 2, notificationContainerElement.find( '> ul > li' ).length );
     151                        assert.notEqual( 'none', notificationContainerElement.css( 'display' ) );
     152                        assert.equal( 2, _.size( control.notifications._value ) );
     153                        assert.equal( 1, _.size( control.settings['default'].notifications._value ) );
     154
     155                        control.notifications.remove( controlOnlyNotification.code );
     156                        control.renderNotifications();
     157                        assert.equal( 1, notificationContainerElement.find( '> ul > li' ).length );
     158                        assert.notEqual( 'none', notificationContainerElement.css( 'display' ) );
     159
     160                        control.settings['default'].notifications.remove( settingNotification.code );
     161                        control.renderNotifications();
     162                        assert.equal( 0, notificationContainerElement.find( '> ul > li' ).length );
     163                        assert.ok( notificationContainerElement.is( ':animated' ) ); // It is being slid down.
     164                        notificationContainerElement.stop().hide(); // Clean up.
     165
     166                        doneEmbedded();
     167                } );
     168        } );
    102169
    103170        module( 'Customizer control without associated settings' );
    104171        test( 'Control can be created without settings', function() {