Make WordPress Core

Changeset 41374


Ignore:
Timestamp:
09/12/2017 07:02:49 AM (9 years ago)
Author:
westonruter
Message:

Customize: Add global notifications area.

  • Displays an error notification in the global area when a save attempt is rejected due to invalid settings. An error notification is also displayed when saving fails due to a network error or server error.
  • Introduces wp.customize.Notifications subclass of wp.customize.Values to contain instances of wp.customize.Notification and manage their rendering into a container.
  • Exposes the global notification area as wp.customize.notifications collection instance.
  • Updates the notifications object on Control to use Notifications rather than Values and to re-use the rendering logic from the former. The old Control#renderNotifications method is deprecated.
  • Allows notifications to be dismissed by instantiating them with a dismissible property.
  • Allows wp.customize.Notification to be extended with custom templates and render functions.
  • Triggers a removed event on wp.customize.Values instances _after_ a value has been removed from the collection.

Props delawski, westonruter, karmatosed, celloexpressions, Fab1en, melchoyce, Kelderic, afercia, adamsilverstein.
See #34893, #39896.
Fixes #35210, #31582, #37727, #37269.

Location:
trunk
Files:
10 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/css/customize-controls.css

    r41368 r41374  
    767767        margin: 4px 0 8px 0;
    768768        padding: 0;
    769         display: none;
    770769        cursor: default;
    771770}
    … …  
    797796.customize-control-text.has-error input {
    798797        outline: 2px solid #dc3232;
     798}
     799
     800#customize-controls #customize-notifications-area {
     801        position: absolute;
     802        top: 46px;
     803        width: 100%;
     804        max-height: 210px;
     805        overflow-x: hidden;
     806        overflow-y: auto;
     807        border-bottom: 1px solid #ddd;
     808        display: block;
     809        padding: 0;
     810        margin: 0;
     811}
     812
     813#customize-controls #customize-notifications-area > ul,
     814#customize-controls #customize-notifications-area .notice {
     815        margin: 0;
     816}
     817#customize-controls #customize-notifications-area .notice {
     818        padding: 9px 14px;
     819}
     820#customize-controls #customize-notifications-area .notice.is-dismissible {
     821        padding-right: 38px;
     822}
     823#customize-controls #customize-notifications-area .notice + .notice {
     824        margin-top: 1px;
    799825}
    800826
  • trunk/src/wp-admin/customize.php

    r40704 r41374  
    152152
    153153                <div id="widgets-right" class="wp-clearfix"><!-- For Widget Customizer, many widgets try to look for instances under div#widgets-right, so we have to add that ID to a container div in the Customizer for compat -->
    154                 <div class="wp-full-overlay-sidebar-content" tabindex="-1">
    155                         <div id="customize-info" class="accordion-section customize-info">
    156                                 <div class="accordion-section-title">
    157                                         <span class="preview-notice"><?php
    158                                                 echo sprintf( __( 'You are customizing %s' ), '<strong class="panel-title site-title">' . get_bloginfo( 'name', 'display' ) . '</strong>' );
    159                                         ?></span>
    160                                         <button type="button" class="customize-help-toggle dashicons dashicons-editor-help" aria-expanded="false"><span class="screen-reader-text"><?php _e( 'Help' ); ?></span></button>
     154                        <div id="customize-notifications-area" class="customize-control-notifications-container">
     155                                <ul></ul>
     156                        </div>
     157                        <div class="wp-full-overlay-sidebar-content" tabindex="-1">
     158                                <div id="customize-info" class="accordion-section customize-info">
     159                                        <div class="accordion-section-title">
     160                                                <span class="preview-notice"><?php
     161                                                        echo sprintf( __( 'You are customizing %s' ), '<strong class="panel-title site-title">' . get_bloginfo( 'name', 'display' ) . '</strong>' );
     162                                                ?></span>
     163                                                <button type="button" class="customize-help-toggle dashicons dashicons-editor-help" aria-expanded="false"><span class="screen-reader-text"><?php _e( 'Help' ); ?></span></button>
     164                                        </div>
     165                                        <div class="customize-panel-description"><?php
     166                                                _e( 'The Customizer allows you to preview changes to your site before publishing them. You can navigate to different pages on your site within the preview. Edit shortcuts are shown for some editable elements.' );
     167                                        ?></div>
    161168                                </div>
    162                                 <div class="customize-panel-description"><?php
    163                                         _e( 'The Customizer allows you to preview changes to your site before publishing them. You can navigate to different pages on your site within the preview. Edit shortcuts are shown for some editable elements.' );
    164                                 ?></div>
     169
     170                                <div id="customize-theme-controls">
     171                                        <ul class="customize-pane-parent"><?php // Panels and sections are managed here via JavaScript ?></ul>
     172                                </div>
    165173                        </div>
    166 
    167                         <div id="customize-theme-controls">
    168                                 <ul class="customize-pane-parent"><?php // Panels and sections are managed here via JavaScript ?></ul>
    169                         </div>
    170                 </div>
    171174                </div>
    172175
  • trunk/src/wp-admin/js/customize-controls.js

    r41368 r41374  
    1 /* global _wpCustomizeHeader, _wpCustomizeBackground, _wpMediaViewsL10n, MediaElementPlayer */
     1/* global _wpCustomizeHeader, _wpCustomizeBackground, _wpMediaViewsL10n, MediaElementPlayer, console */
    22(function( exports, $ ){
    33        var Container, focus, normalizedTransitionendEventName, api = wp.customize;
     4
     5        /**
     6         * A collection of observable notifications.
     7         *
     8         * @since 4.9.0
     9         * @class
     10         * @augments wp.customize.Values
     11         */
     12        api.Notifications = api.Values.extend({
     13
     14                /**
     15                 * Whether the alternative style should be used.
     16                 *
     17                 * @since 4.9.0
     18                 * @type {boolean}
     19                 */
     20                alt: false,
     21
     22                /**
     23                 * The default constructor for items of the collection.
     24                 *
     25                 * @since 4.9.0
     26                 * @type {object}
     27                 */
     28                defaultConstructor: api.Notification,
     29
     30                /**
     31                 * Initialize notifications area.
     32                 *
     33                 * @since 4.9.0
     34                 * @constructor
     35                 * @param {object}  options - Options.
     36                 * @param {jQuery}  [options.container] - Container element for notifications. This can be injected later.
     37                 * @param {boolean} [options.alt] - Whether alternative style should be used when rendering notifications.
     38                 * @returns {void}
     39                 * @this {wp.customize.Notifications}
     40                 */
     41                initialize: function( options ) {
     42                        var collection = this;
     43
     44                        api.Values.prototype.initialize.call( collection, options );
     45
     46                        // Keep track of the order in which the notifications were added for sorting purposes.
     47                        collection._addedIncrement = 0;
     48                        collection._addedOrder = {};
     49
     50                        // Trigger change event when notification is added or removed.
     51                        collection.bind( 'add', function( notification ) {
     52                                collection.trigger( 'change', notification );
     53                        });
     54                        collection.bind( 'removed', function( notification ) {
     55                                collection.trigger( 'change', notification );
     56                        });
     57                },
     58
     59                /**
     60                 * Get the number of notifications added.
     61                 *
     62                 * @since 4.9.0
     63                 * @return {number} Count of notifications.
     64                 */
     65                count: function() {
     66                        return _.size( this._value );
     67                },
     68
     69                /**
     70                 * Add notification to the collection.
     71                 *
     72                 * @since 4.9.0
     73                 * @param {string} code - Notification code.
     74                 * @param {object} params - Notification params.
     75                 * @return {api.Notification} Added instance (or existing instance if it was already added).
     76                 */
     77                add: function( code, params ) {
     78                        var collection = this;
     79                        if ( ! collection.has( code ) ) {
     80                                collection._addedIncrement += 1;
     81                                collection._addedOrder[ code ] = collection._addedIncrement;
     82                        }
     83                        return api.Values.prototype.add.call( this, code, params );
     84                },
     85
     86                /**
     87                 * Add notification to the collection.
     88                 *
     89                 * @since 4.9.0
     90                 * @param {string} code - Notification code to remove.
     91                 * @return {api.Notification} Added instance (or existing instance if it was already added).
     92                 */
     93                remove: function( code ) {
     94                        var collection = this;
     95                        delete collection._addedOrder[ code ];
     96                        return api.Values.prototype.remove.call( this, code );
     97                },
     98
     99                /**
     100                 * Get list of notifications.
     101                 *
     102                 * Notifications may be sorted by type followed by added time.
     103                 *
     104                 * @since 4.9.0
     105                 * @param {object}  args - Args.
     106                 * @param {boolean} [args.sort=false] - Whether to return the notifications sorted.
     107                 * @return {Array.<wp.customize.Notification>} Notifications.
     108                 * @this {wp.customize.Notifications}
     109                 */
     110                get: function( args ) {
     111                        var collection = this, notifications, errorTypePriorities, params;
     112                        notifications = _.values( collection._value );
     113
     114                        params = _.extend(
     115                                { sort: false },
     116                                args
     117                        );
     118
     119                        if ( params.sort ) {
     120                                errorTypePriorities = { error: 4, warning: 3, success: 2, info: 1 };
     121                                notifications.sort( function( a, b ) {
     122                                        var aPriority = 0, bPriority = 0;
     123                                        if ( ! _.isUndefined( errorTypePriorities[ a.type ] ) ) {
     124                                                aPriority = errorTypePriorities[ a.type ];
     125                                        }
     126                                        if ( ! _.isUndefined( errorTypePriorities[ b.type ] ) ) {
     127                                                bPriority = errorTypePriorities[ b.type ];
     128                                        }
     129                                        if ( aPriority !== bPriority ) {
     130                                                return bPriority - aPriority; // Show errors first.
     131                                        }
     132                                        return collection._addedOrder[ b.code ] - collection._addedOrder[ a.code ]; // Show newer notifications higher.
     133                                });
     134                        }
     135
     136                        return notifications;
     137                },
     138
     139                /**
     140                 * Render notifications area.
     141                 *
     142                 * @since 4.9.0
     143                 * @returns {void}
     144                 * @this {wp.customize.Notifications}
     145                 */
     146                render: function() {
     147                        var collection = this,
     148                                notifications,
     149                                renderedNotificationContainers,
     150                                prevRenderedCodes,
     151                                nextRenderedCodes,
     152                                addedCodes,
     153                                removedCodes,
     154                                listElement;
     155
     156                        // Short-circuit if there are no container to render into.
     157                        if ( ! collection.container || ! collection.container.length ) {
     158                                return;
     159                        }
     160                        listElement = collection.container.children( 'ul' ).first();
     161                        if ( ! listElement.length ) {
     162                                listElement = $( '<ul></ul>' );
     163                                collection.container.append( listElement );
     164                        }
     165
     166                        notifications = collection.get( { sort: true } );
     167
     168                        renderedNotificationContainers = {};
     169                        listElement.find( '> [data-code]' ).each( function() {
     170                                renderedNotificationContainers[ $( this ).data( 'code' ) ] = $( this );
     171                        });
     172
     173                        collection.container.toggle( 0 !== notifications.length );
     174
     175                        nextRenderedCodes = _.pluck( notifications, 'code' );
     176                        prevRenderedCodes = _.keys( renderedNotificationContainers );
     177
     178                        // Short-circuit if there are no notifications added.
     179                        if ( _.isEqual( nextRenderedCodes, prevRenderedCodes ) ) {
     180                                return;
     181                        }
     182
     183                        addedCodes = _.difference( nextRenderedCodes, prevRenderedCodes );
     184                        removedCodes = _.difference( prevRenderedCodes, nextRenderedCodes );
     185
     186                        // Remove notifications that have been removed.
     187                        _.each( renderedNotificationContainers, function( renderedContainer, code ) {
     188                                if ( -1 !== _.indexOf( removedCodes, code ) ) {
     189                                        renderedContainer.remove(); // @todo Consider slideUp as enhancement.
     190                                }
     191                        });
     192
     193                        // Add all notifications in the sorted order.
     194                        _.each( notifications, function( notification ) {
     195                                var notificationContainer = renderedNotificationContainers[ notification.code ];
     196                                if ( notificationContainer ) {
     197                                        listElement.append( notificationContainer );
     198                                } else {
     199                                        notificationContainer = $( notification.render() );
     200                                        listElement.append( notificationContainer ); // @todo Consider slideDown() as enhancement.
     201                                        if ( wp.a11y ) {
     202                                                wp.a11y.speak( notification.message, 'assertive' );
     203                                        }
     204                                }
     205                        });
     206
     207                        collection.trigger( 'rendered' );
     208                }
     209        });
    4210
    5211        /**
    … …  
    18842090                        control.active = new api.Value();
    18852091                        control.activeArgumentsQueue = [];
    1886                         control.notifications = new api.Values({ defaultConstructor: api.Notification });
     2092                        control.notifications = new api.Notifications({
     2093                                alt: control.altNotice
     2094                        });
    18872095
    18882096                        control.elements = [];
    … …  
    19742182                        // After the control is embedded on the page, invoke the "ready" method.
    19752183                        control.deferred.embedded.done( function () {
    1976                                 /*
    1977                                  * Note that this debounced/deferred rendering is needed for two reasons:
    1978                                  * 1) The 'remove' event is triggered just _before_ the notification is actually removed.
    1979                                  * 2) Improve performance when adding/removing multiple notifications at a time.
    1980                                  */
    1981                                 var debouncedRenderNotifications = _.debounce( function renderNotifications() {
    1982                                         control.renderNotifications();
     2184                                var renderNotifications = function() {
     2185                                        control.notifications.render();
     2186                                };
     2187                                control.notifications.container = control.getNotificationsContainerElement();
     2188                                control.notifications.bind( 'rendered', function() {
     2189                                        var notifications = control.notifications.get();
     2190                                        control.container.toggleClass( 'has-notifications', 0 !== notifications.length );
     2191                                        control.container.toggleClass( 'has-error', 0 !== _.where( notifications, { type: 'error' } ).length );
    19832192                                } );
    1984                                 control.notifications.bind( 'add', function( notification ) {
    1985                                         wp.a11y.speak( notification.message, 'assertive' );
    1986                                         debouncedRenderNotifications();
    1987                                 } );
    1988                                 control.notifications.bind( 'remove', debouncedRenderNotifications );
    1989                                 control.renderNotifications();
    1990 
     2193                                renderNotifications();
     2194                                control.notifications.bind( 'change', _.debounce( renderNotifications ) );
    19912195                                control.ready();
    19922196                        });
    … …  
    20922296                 * of rendering notifications.
    20932297                 *
     2298                 * @deprecated in favor of `control.notifications.render()`
    20942299                 * @since 4.6.0
    20952300                 * @this {wp.customize.Control}
    … …  
    20972302                renderNotifications: function() {
    20982303                        var control = this, container, notifications, hasError = false;
     2304
     2305                        if ( 'undefined' !== typeof console && console.warn ) {
     2306                                console.warn( '[DEPRECATED] wp.customize.Control.prototype.renderNotifications() is deprecated in favor of instantating a wp.customize.Notifications and calling its render() method.' );
     2307                        }
     2308
    20992309                        container = control.getNotificationsContainerElement();
    21002310                        if ( ! container || ! container.length ) {
    … …  
    34273637        api.section = new api.Values({ defaultConstructor: api.Section });
    34283638        api.panel = new api.Values({ defaultConstructor: api.Panel });
     3639
     3640        // Create the collection for global Notifications.
     3641        api.notifications = new api.Notifications();
    34293642
    34303643        /**
    … …  
    45024715                                        } );
    45034716
     4717                                        // Remove notifications that were added due to save failures.
     4718                                        api.notifications.each( function( notification ) {
     4719                                                if ( notification.saveFailure ) {
     4720                                                        api.notifications.remove( notification.code );
     4721                                                }
     4722                                        });
     4723
    45044724                                        request.fail( function ( response ) {
    45054725
    … …  
    45194739                                                                previewer.preview.iframe.show();
    45204740                                                        } );
     4741                                                } else if ( response.code ) {
     4742                                                        api.notifications.add( response.code, new api.Notification( response.code, {
     4743                                                                message: response.message,
     4744                                                                type: 'error',
     4745                                                                dismissible: true,
     4746                                                                fromServer: true,
     4747                                                                saveFailure: true
     4748                                                        } ) );
     4749                                                } else {
     4750                                                        api.notifications.add( 'unknown_error', new api.Notification( 'unknown_error', {
     4751                                                                message: api.l10n.serverSaveError,
     4752                                                                type: 'error',
     4753                                                                dismissible: true,
     4754                                                                fromServer: true,
     4755                                                                saveFailure: true
     4756                                                        } ) );
    45214757                                                }
    45224758
    … …  
    46884924                        values.bind( 'remove', debouncedReflowPaneContents );
    46894925                } );
     4926
     4927                // Set up global notifications area.
     4928                api.bind( 'ready', function setUpGlobalNotificationsArea() {
     4929                        var sidebar, containerHeight, containerInitialTop;
     4930                        api.notifications.container = $( '#customize-notifications-area' );
     4931
     4932                        api.notifications.bind( 'change', _.debounce( function() {
     4933                                api.notifications.render();
     4934                        } ) );
     4935
     4936                        sidebar = $( '.wp-full-overlay-sidebar-content' );
     4937                        api.notifications.bind( 'rendered', function updateSidebarTop() {
     4938                                sidebar.css( 'top', '' );
     4939                                if ( 0 !== api.notifications.count() ) {
     4940                                        containerHeight = api.notifications.container.outerHeight() + 1;
     4941                                        containerInitialTop = parseInt( sidebar.css( 'top' ), 10 );
     4942                                        sidebar.css( 'top', containerInitialTop + containerHeight + 'px' );
     4943                                }
     4944                                api.notifications.trigger( 'sidebarTopUpdated' );
     4945                        });
     4946
     4947                        api.notifications.render();
     4948                });
    46904949
    46914950                // Save and activated states
    … …  
    49725231
    49735232                                var scrollTop = parentContainer.scrollTop(),
    4974                                         isScrollingUp = ( lastScrollTop ) ? scrollTop <= lastScrollTop : true;
    4975 
     5233                                        scrollDirection;
     5234
     5235                                if ( ! lastScrollTop ) {
     5236                                        scrollDirection = 1;
     5237                                } else {
     5238                                        if ( scrollTop === lastScrollTop ) {
     5239                                                scrollDirection = 0;
     5240                                        } else if ( scrollTop > lastScrollTop ) {
     5241                                                scrollDirection = 1;
     5242                                        } else {
     5243                                                scrollDirection = -1;
     5244                                        }
     5245                                }
    49765246                                lastScrollTop = scrollTop;
    4977                                 positionStickyHeader( activeHeader, scrollTop, isScrollingUp );
     5247                                if ( 0 !== scrollDirection ) {
     5248                                        positionStickyHeader( activeHeader, scrollTop, scrollDirection );
     5249                                }
    49785250                        }, 8 ) );
     5251
     5252                        // Update header position on sidebar layout change.
     5253                        api.notifications.bind( 'sidebarTopUpdated', function() {
     5254                                if ( activeHeader && activeHeader.element.hasClass( 'is-sticky' ) ) {
     5255                                        activeHeader.element.css( 'top', parentContainer.css( 'top' ) );
     5256                                }
     5257                        });
    49795258
    49805259                        // Release header element if it is sticky.
    … …  
    49915270                        // Reset position of the sticky header.
    49925271                        resetStickyHeader = function( headerElement, headerParent ) {
    4993                                 headerElement
    4994                                         .removeClass( 'maybe-sticky is-in-view' )
    4995                                         .css( {
    4996                                                 width: '',
    4997                                                 top: ''
    4998                                         } );
    4999                                 headerParent.css( 'padding-top', '' );
     5272                                if ( headerElement.hasClass( 'is-in-view' ) ) {
     5273                                        headerElement
     5274                                                .removeClass( 'maybe-sticky is-in-view' )
     5275                                                .css( {
     5276                                                        width: '',
     5277                                                        top:   ''
     5278                                                } );
     5279                                        headerParent.css( 'padding-top', '' );
     5280                                }
    50005281                        };
    50015282
    … …  
    50245305                         * @access private
    50255306                         *
    5026                          * @param {object}  header        Header.
    5027                          * @param {number}  scrollTop    Scroll top.
    5028                          * @param {boolean} isScrollingUp Is scrolling up?
     5307                         * @param {object} header - Header.
     5308                         * @param {number} scrollTop - Scroll top.
     5309                         * @param {number} scrollDirection - Scroll direction, negative number being up and positive being down.
    50295310                         * @returns {void}
    50305311                         */
    5031                         positionStickyHeader = function( header, scrollTop, isScrollingUp ) {
     5312                        positionStickyHeader = function( header, scrollTop, scrollDirection ) {
    50325313                                var headerElement = header.element,
    50335314                                        headerParent = header.parent,
    … …  
    50365317                                        maybeSticky = headerElement.hasClass( 'maybe-sticky' ),
    50375318                                        isSticky = headerElement.hasClass( 'is-sticky' ),
    5038                                         isInView = headerElement.hasClass( 'is-in-view' );
     5319                                        isInView = headerElement.hasClass( 'is-in-view' ),
     5320                                        isScrollingUp = ( -1 === scrollDirection );
    50395321
    50405322                                // When scrolling down, gradually hide sticky header.
    … …  
    50795361                                                        .addClass( 'is-sticky' )
    50805362                                                        .css( {
    5081                                                                 top:   '',
     5363                                                                top:   parentContainer.css( 'top' ),
    50825364                                                                width: headerParent.outerWidth() + 'px'
    50835365                                                        } );
  • trunk/src/wp-admin/js/customize-widgets.js

    r41249 r41374  
    551551                        control.widgetContentEmbedded = true;
    552552
     553                        // Update the notification container element now that the widget content has been embedded.
     554                        control.notifications.container = control.getNotificationsContainerElement();
     555                        control.notifications.render();
     556
    553557                        widgetContent = $( control.params.widget_content );
    554558                        control.container.find( '.widget-content:first' ).append( widgetContent );
  • trunk/src/wp-includes/class-wp-customize-manager.php

    r41372 r41374  
    349349                add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_control_scripts' ) );
    350350
    351                 // Render Panel, Section, and Control templates.
     351                // Render Common, Panel, Section, and Control templates.
    352352                add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_panel_templates' ), 1 );
    353353                add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_section_templates' ), 1 );
    … …  
    23562356                        $response = array(
    23572357                                'setting_validities' => $setting_validities,
    2358                                 'message' => sprintf( _n( 'There is %s invalid setting.', 'There are %s invalid settings.', $invalid_setting_count ), number_format_i18n( $invalid_setting_count ) ),
     2358                                /* translators: placeholder is number of invalid settings */
     2359                                'message' => sprintf( _n( 'Unable to save due to %s invalid setting.', 'Unable to save due to %s invalid settings.', $invalid_setting_count ), number_format_i18n( $invalid_setting_count ) ),
    23592360                        );
    23602361                        return new WP_Error( 'transaction_fail', '', $response );
    … …  
    31843185                        $control->print_template();
    31853186                }
     3187
     3188                ?>
     3189                <script type="text/html" id="tmpl-customize-notification">
     3190                        <li class="notice notice-{{ data.type || 'info' }} {{ data.alt ? 'notice-alt' : '' }} {{ data.dismissible ? 'is-dismissible' : '' }}" data-code="{{ data.code }}" data-type="{{ data.type }}">
     3191                                {{{ data.message || data.code }}}
     3192                                <# if ( data.dismissible ) { #>
     3193                                        <button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php _e( 'Dismiss' ); ?></span></button>
     3194                                <# } #>
     3195                        </li>
     3196                </script>
     3197
     3198                <?php
     3199                /* The following template is obsolete in core but retained for plugins. */
    31863200                ?>
    31873201                <script type="text/html" id="tmpl-customize-control-notifications">
  • trunk/src/wp-includes/js/customize-base.js

    r41351 r41374  
    434434                 */
    435435                remove: function( id ) {
    436                         var value;
    437 
    438                         if ( this.has( id ) ) {
    439                                 value = this.value( id );
     436                        var value = this.value( id );
     437
     438                        if ( value ) {
     439
     440                                // Trigger event right before the element is removed from the collection.
    440441                                this.trigger( 'remove', value );
    441                                 if ( value.extended( api.Value ) )
     442
     443                                if ( value.extended( api.Value ) ) {
    442444                                        value.unbind( this._change );
     445                                }
    443446                                delete value.parent;
    444447                        }
    … …  
    446449                        delete this._value[ id ];
    447450                        delete this._deferreds[ id ];
     451
     452                        // Trigger removed event after the item has been eliminated from the collection.
     453                        if ( value ) {
     454                                this.trigger( 'removed', value );
     455                        }
    448456                },
    449457
    … …  
    791799         */
    792800        api.Notification = api.Class.extend(/** @lends wp.customize.Notification.prototype */{
     801
     802                /**
     803                 * Template function for rendering the notification.
     804                 *
     805                 * This will be populated with template option or else it will be populated with template from the ID.
     806                 *
     807                 * @since 4.9.0
     808                 * @var {Function}
     809                 */
     810                template: null,
     811
     812                /**
     813                 * ID for the template to render the notification.
     814                 *
     815                 * @since 4.9.0
     816                 * @var {string}
     817                 */
     818                templateId: 'customize-notification',
     819
     820                /**
     821                 * Initialize notification.
     822                 *
     823                 * @since 4.9.0
     824                 *
     825                 * @param {string}   code - Notification code.
     826                 * @param {object}   params - Notification parameters.
     827                 * @param {string}   params.message - Message.
     828                 * @param {string}   [params.type=error] - Type.
     829                 * @param {string}   [params.setting] - Related setting ID.
     830                 * @param {Function} [params.template] - Function for rendering template. If not provided, this will come from templateId.
     831                 * @param {string}   [params.templateId] - ID for template to render the notification.
     832                 * @param {boolean}  [params.dismissible] - Whether the notification can be dismissed.
     833                 */
    793834                initialize: function( code, params ) {
    794835                        var _params;
    … …  
    800841                                        fromServer: false,
    801842                                        data: null,
    802                                         setting: null
     843                                        setting: null,
     844                                        template: null,
     845                                        dismissible: false
    803846                                },
    804847                                params
    … …  
    806849                        delete _params.code;
    807850                        _.extend( this, _params );
     851                },
     852
     853                /**
     854                 * Render the notification.
     855                 *
     856                 * @since 4.9.0
     857                 *
     858                 * @returns {jQuery} Notification container element.
     859                 */
     860                render: function() {
     861                        var notification = this, container, data;
     862                        if ( ! notification.template ) {
     863                                notification.template = wp.template( notification.templateId );
     864                        }
     865                        data = _.extend( {}, notification, {
     866                                alt: notification.parent && notification.parent.alt
     867                        } );
     868                        container = $( notification.template( data ) );
     869
     870                        if ( notification.dismissible ) {
     871                                container.find( '.notice-dismiss' ).on( 'click', function() {
     872                                        if ( notification.parent ) {
     873                                                notification.parent.remove( notification.code );
     874                                        } else {
     875                                                container.remove();
     876                                        }
     877                                });
     878                        }
     879
     880                        return container;
    808881                }
    809882        });
  • trunk/src/wp-includes/script-loader.php

    r41360 r41374  
    547547                'expandSidebar'      => _x( 'Show Controls', 'label for hide controls button without length constraints' ),
    548548                'untitledBlogName'   => __( '(Untitled)' ),
     549                'serverSaveError'    => __( 'Failed connecting to the server. Please try saving again.' ),
    549550                // Used for overriding the file types allowed in plupload.
    550551                'allowedFiles'       => __( 'Allowed Files' ),
  • trunk/tests/qunit/index.html

    r41206 r41374  
    210210                                <# } ); #>
    211211                        </ul>
     212                </script>
     213                <script type="text/html" id="tmpl-customize-notification">
     214                        <li class="notice notice-{{ data.type || 'info' }} {{ data.altNotice ? 'notice-alt' : '' }} {{ data.dismissible ? 'is-dismissible' : '' }}" data-code="{{ data.code }}" data-type="{{ data.type }}">
     215                                {{{ data.message || data.code }}}
     216                                <# if ( data.dismissible ) { #>
     217                                        <button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php _e( 'Dismiss' ); ?></span></button>
     218                                <# } #>
     219                        </li>
    212220                </script>
    213221
    … …  
    387395                <div hidden>
    388396                        <div id="customize-preview"></div>
     397                        <div id="customize-notifications-test"><ul></ul></div>
    389398                </div>
    390399
  • trunk/tests/qunit/wp-admin/js/customize-base.js

    r38810 r41374  
    33jQuery( function( $ ) {
    44        var FooSuperClass, BarSubClass, foo, bar, ConstructorTestClass, newConstructor, constructorTest, $mockElement, mockString,
    5         firstInitialValue, firstValueInstance, wasCallbackFired, mockValueCallback;
     5        firstInitialValue, firstValueInstance, valuesInstance, wasCallbackFired, mockValueCallback;
    66
    77        module( 'Customize Base: Class' );
    … …  
    160160        });
    161161
     162        module( 'Customize Base: Values Class' );
     163
     164        valuesInstance = new wp.customize.Values();
     165
     166        test( 'Correct events are triggered when adding to or removing from Values collection', function() {
     167                var hasFooOnAdd = false,
     168                        hasFooOnRemove = false,
     169                        hasFooOnRemoved = true,
     170                        valuePassedToAdd = false,
     171                        valuePassedToRemove = false,
     172                        valuePassedToRemoved = false,
     173                        wasEventFiredOnRemoval = false,
     174                        fooValue = new wp.customize.Value( 'foo' );
     175
     176                // Test events when adding new value.
     177                valuesInstance.bind( 'add', function( value ) {
     178                        hasFooOnAdd = valuesInstance.has( 'foo' );
     179                        valuePassedToAdd = value;
     180                } );
     181                valuesInstance.add( 'foo', fooValue );
     182                ok( hasFooOnAdd );
     183                equal( valuePassedToAdd.get(), fooValue.get() );
     184
     185                // Test events when removing the value.
     186                valuesInstance.bind( 'remove', function( value ) {
     187                        hasFooOnRemove = valuesInstance.has( 'foo' );
     188                        valuePassedToRemove = value;
     189                        wasEventFiredOnRemoval = true;
     190                } );
     191                valuesInstance.bind( 'removed', function( value ) {
     192                        hasFooOnRemoved = valuesInstance.has( 'foo' );
     193                        valuePassedToRemoved = value;
     194                        wasEventFiredOnRemoval = true;
     195                } );
     196                valuesInstance.remove( 'foo' );
     197                ok( hasFooOnRemove );
     198                equal( valuePassedToRemove.get(), fooValue.get() );
     199                ok( ! hasFooOnRemoved );
     200                equal( valuePassedToRemoved.get(), fooValue.get() );
     201
     202                // Confirm no events are fired when nonexistent value is removed.
     203                wasEventFiredOnRemoval = false;
     204                valuesInstance.remove( 'bar' );
     205                ok( ! wasEventFiredOnRemoval );
     206        });
     207
    162208        module( 'Customize Base: Notification' );
    163209        test( 'Notification object exists and has expected properties', function ( assert ) {
  • trunk/tests/qunit/wp-admin/js/customize-controls.js

    r38993 r41374  
    8383        };
    8484
     85        module( 'Customizer notifications collection' );
     86        test( 'Notifications collection exists', function() {
     87                ok( wp.customize.notifications );
     88                equal( wp.customize.notifications.defaultConstructor, wp.customize.Notification );
     89        } );
     90
     91        test( 'Notification objects are rendered as part of notifications collection', function() {
     92                var container = jQuery( '#customize-notifications-test' ), items, collection;
     93
     94                collection = new wp.customize.Notifications({
     95                        container: container
     96                });
     97                collection.add( 'mycode-1', new wp.customize.Notification( 'mycode-1' ) );
     98                collection.render();
     99                items = collection.container.find( 'li' );
     100                equal( items.length, 1 );
     101                equal( items.first().data( 'code' ), 'mycode-1' );
     102
     103                collection.add( 'mycode-2', new wp.customize.Notification( 'mycode-2', {
     104                        dismissible: true
     105                } ) );
     106                collection.render();
     107                items = collection.container.find( 'li' );
     108                equal( items.length, 2 );
     109                equal( items.first().data( 'code' ), 'mycode-2' );
     110                equal( items.last().data( 'code' ), 'mycode-1' );
     111
     112                equal( items.first().find( '.notice-dismiss' ).length, 1 );
     113                equal( items.last().find( '.notice-dismiss' ).length, 0 );
     114
     115                collection.remove( 'mycode-2' );
     116                collection.render();
     117                items = collection.container.find( 'li' );
     118                equal( items.length, 1 );
     119                equal( items.first().data( 'code' ), 'mycode-1' );
     120
     121                collection.remove( 'mycode-1' );
     122                collection.render();
     123                ok( collection.container.is( ':hidden' ), 'Notifications area is hidden.' );
     124        } );
     125
    85126        module( 'Customizer Previewed Device' );
    86127        test( 'Previewed device defaults to desktop.', function () {
    … …  
    145186                        assert.ok( notificationContainerElement.is( '.customize-control-notifications-container' ) );
    146187                        assert.equal( 0, notificationContainerElement.find( '> ul > li' ).length );
    147                         assert.equal( 'none', notificationContainerElement.css( 'display' ) );
     188                        assert.equal( 0, notificationContainerElement.height() );
    148189
    149190                        settingNotification = new wp.customize.Notification( 'setting_invalidity', 'Invalid setting' );
    … …  
    153194
    154195                        // Note that renderNotifications is being called manually here since rendering normally happens asynchronously.
    155                         control.renderNotifications();
     196                        control.notifications.render();
    156197
    157198                        assert.equal( 2, notificationContainerElement.find( '> ul > li' ).length );
    … …  
    161202
    162203                        control.notifications.remove( controlOnlyNotification.code );
    163                         control.renderNotifications();
     204                        control.notifications.render();
    164205                        assert.equal( 1, notificationContainerElement.find( '> ul > li' ).length );
    165206                        assert.notEqual( 'none', notificationContainerElement.css( 'display' ) );
    166207
    167208                        control.settings['default'].notifications.remove( settingNotification.code );
    168                         control.renderNotifications();
     209                        control.notifications.render();
    169210                        assert.equal( 0, notificationContainerElement.find( '> ul > li' ).length );
    170                         assert.ok( notificationContainerElement.is( ':animated' ) ); // It is being slid down.
    171211                        notificationContainerElement.stop().hide(); // Clean up.
    172212
Note: See TracChangeset for help on using the changeset viewer.