WordPress.org

Make WordPress Core

Changeset 41374


Ignore:
Timestamp:
09/12/17 07:02:49 (2 months 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.