<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[41374] trunk: Customize: Add global notifications area.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/41374">41374</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/41374","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>westonruter</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2017-09-12 07:02:49 +0000 (Tue, 12 Sep 2017)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>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 <a href="https://core.trac.wordpress.org/ticket/34893">#34893</a>, <a href="https://core.trac.wordpress.org/ticket/39896">#39896</a>.
Fixes <a href="https://core.trac.wordpress.org/ticket/35210">#35210</a>, <a href="https://core.trac.wordpress.org/ticket/31582">#31582</a>, <a href="https://core.trac.wordpress.org/ticket/37727">#37727</a>, <a href="https://core.trac.wordpress.org/ticket/37269">#37269</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpadmincsscustomizecontrolscss">trunk/src/wp-admin/css/customize-controls.css</a></li>
<li><a href="#trunksrcwpadmincustomizephp">trunk/src/wp-admin/customize.php</a></li>
<li><a href="#trunksrcwpadminjscustomizecontrolsjs">trunk/src/wp-admin/js/customize-controls.js</a></li>
<li><a href="#trunksrcwpadminjscustomizewidgetsjs">trunk/src/wp-admin/js/customize-widgets.js</a></li>
<li><a href="#trunksrcwpincludesclasswpcustomizemanagerphp">trunk/src/wp-includes/class-wp-customize-manager.php</a></li>
<li><a href="#trunksrcwpincludesjscustomizebasejs">trunk/src/wp-includes/js/customize-base.js</a></li>
<li><a href="#trunksrcwpincludesscriptloaderphp">trunk/src/wp-includes/script-loader.php</a></li>
<li><a href="#trunktestsqunitindexhtml">trunk/tests/qunit/index.html</a></li>
<li><a href="#trunktestsqunitwpadminjscustomizebasejs">trunk/tests/qunit/wp-admin/js/customize-base.js</a></li>
<li><a href="#trunktestsqunitwpadminjscustomizecontrolsjs">trunk/tests/qunit/wp-admin/js/customize-controls.js</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpadmincsscustomizecontrolscss"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/css/customize-controls.css</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/css/customize-controls.css     2017-09-12 03:33:58 UTC (rev 41373)
+++ trunk/src/wp-admin/css/customize-controls.css       2017-09-12 07:02:49 UTC (rev 41374)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -766,7 +766,6 @@
</span><span class="cx" style="display: block; padding: 0 10px"> #customize-controls .customize-control-notifications-container { /* Scoped to #customize-controls for specificity over notification styles in common.css. */
</span><span class="cx" style="display: block; padding: 0 10px">        margin: 4px 0 8px 0;
</span><span class="cx" style="display: block; padding: 0 10px">        padding: 0;
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        display: none;
</del><span class="cx" style="display: block; padding: 0 10px">         cursor: default;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -798,6 +797,33 @@
</span><span class="cx" style="display: block; padding: 0 10px">        outline: 2px solid #dc3232;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+#customize-controls #customize-notifications-area {
+       position: absolute;
+       top: 46px;
+       width: 100%;
+       max-height: 210px;
+       overflow-x: hidden;
+       overflow-y: auto;
+       border-bottom: 1px solid #ddd;
+       display: block;
+       padding: 0;
+       margin: 0;
+}
+
+#customize-controls #customize-notifications-area > ul,
+#customize-controls #customize-notifications-area .notice {
+       margin: 0;
+}
+#customize-controls #customize-notifications-area .notice {
+       padding: 9px 14px;
+}
+#customize-controls #customize-notifications-area .notice.is-dismissible {
+       padding-right: 38px;
+}
+#customize-controls #customize-notifications-area .notice + .notice {
+       margin-top: 1px;
+}
+
</ins><span class="cx" style="display: block; padding: 0 10px"> /* Style for custom settings */
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span></span></pre></div>
<a id="trunksrcwpadmincustomizephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/customize.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/customize.php  2017-09-12 03:33:58 UTC (rev 41373)
+++ trunk/src/wp-admin/customize.php    2017-09-12 07:02:49 UTC (rev 41374)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -151,24 +151,27 @@
</span><span class="cx" style="display: block; padding: 0 10px">                </div>
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                <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 -->
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                <div class="wp-full-overlay-sidebar-content" tabindex="-1">
-                       <div id="customize-info" class="accordion-section customize-info">
-                               <div class="accordion-section-title">
-                                       <span class="preview-notice"><?php
-                                               echo sprintf( __( 'You are customizing %s' ), '<strong class="panel-title site-title">' . get_bloginfo( 'name', 'display' ) . '</strong>' );
-                                       ?></span>
-                                       <button type="button" class="customize-help-toggle dashicons dashicons-editor-help" aria-expanded="false"><span class="screen-reader-text"><?php _e( 'Help' ); ?></span></button>
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 <div id="customize-notifications-area" class="customize-control-notifications-container">
+                               <ul></ul>
+                       </div>
+                       <div class="wp-full-overlay-sidebar-content" tabindex="-1">
+                               <div id="customize-info" class="accordion-section customize-info">
+                                       <div class="accordion-section-title">
+                                               <span class="preview-notice"><?php
+                                                       echo sprintf( __( 'You are customizing %s' ), '<strong class="panel-title site-title">' . get_bloginfo( 'name', 'display' ) . '</strong>' );
+                                               ?></span>
+                                               <button type="button" class="customize-help-toggle dashicons dashicons-editor-help" aria-expanded="false"><span class="screen-reader-text"><?php _e( 'Help' ); ?></span></button>
+                                       </div>
+                                       <div class="customize-panel-description"><?php
+                                               _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.' );
+                                       ?></div>
</ins><span class="cx" style="display: block; padding: 0 10px">                                 </div>
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                <div class="customize-panel-description"><?php
-                                       _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.' );
-                               ?></div>
-                       </div>
</del><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        <div id="customize-theme-controls">
-                               <ul class="customize-pane-parent"><?php // Panels and sections are managed here via JavaScript ?></ul>
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         <div id="customize-theme-controls">
+                                       <ul class="customize-pane-parent"><?php // Panels and sections are managed here via JavaScript ?></ul>
+                               </div>
</ins><span class="cx" style="display: block; padding: 0 10px">                         </div>
</span><span class="cx" style="display: block; padding: 0 10px">                </div>
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                </div>
</del><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                <div id="customize-footer-actions" class="wp-full-overlay-footer">
</span><span class="cx" style="display: block; padding: 0 10px">                        <button type="button" class="collapse-sidebar button" aria-expanded="true" aria-label="<?php echo esc_attr( _x( 'Hide Controls', 'label for hide controls button without length constraints' ) ); ?>">
</span></span></pre></div>
<a id="trunksrcwpadminjscustomizecontrolsjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/js/customize-controls.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/js/customize-controls.js       2017-09-12 03:33:58 UTC (rev 41373)
+++ trunk/src/wp-admin/js/customize-controls.js 2017-09-12 07:02:49 UTC (rev 41374)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,8 +1,214 @@
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-/* global _wpCustomizeHeader, _wpCustomizeBackground, _wpMediaViewsL10n, MediaElementPlayer */
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/* global _wpCustomizeHeader, _wpCustomizeBackground, _wpMediaViewsL10n, MediaElementPlayer, console */
</ins><span class="cx" style="display: block; padding: 0 10px"> (function( exports, $ ){
</span><span class="cx" style="display: block; padding: 0 10px">        var Container, focus, normalizedTransitionendEventName, api = wp.customize;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * A collection of observable notifications.
+        *
+        * @since 4.9.0
+        * @class
+        * @augments wp.customize.Values
+        */
+       api.Notifications = api.Values.extend({
+
+               /**
+                * Whether the alternative style should be used.
+                *
+                * @since 4.9.0
+                * @type {boolean}
+                */
+               alt: false,
+
+               /**
+                * The default constructor for items of the collection.
+                *
+                * @since 4.9.0
+                * @type {object}
+                */
+               defaultConstructor: api.Notification,
+
+               /**
+                * Initialize notifications area.
+                *
+                * @since 4.9.0
+                * @constructor
+                * @param {object}  options - Options.
+                * @param {jQuery}  [options.container] - Container element for notifications. This can be injected later.
+                * @param {boolean} [options.alt] - Whether alternative style should be used when rendering notifications.
+                * @returns {void}
+                * @this {wp.customize.Notifications}
+                */
+               initialize: function( options ) {
+                       var collection = this;
+
+                       api.Values.prototype.initialize.call( collection, options );
+
+                       // Keep track of the order in which the notifications were added for sorting purposes.
+                       collection._addedIncrement = 0;
+                       collection._addedOrder = {};
+
+                       // Trigger change event when notification is added or removed.
+                       collection.bind( 'add', function( notification ) {
+                               collection.trigger( 'change', notification );
+                       });
+                       collection.bind( 'removed', function( notification ) {
+                               collection.trigger( 'change', notification );
+                       });
+               },
+
+               /**
+                * Get the number of notifications added.
+                *
+                * @since 4.9.0
+                * @return {number} Count of notifications.
+                */
+               count: function() {
+                       return _.size( this._value );
+               },
+
+               /**
+                * Add notification to the collection.
+                *
+                * @since 4.9.0
+                * @param {string} code - Notification code.
+                * @param {object} params - Notification params.
+                * @return {api.Notification} Added instance (or existing instance if it was already added).
+                */
+               add: function( code, params ) {
+                       var collection = this;
+                       if ( ! collection.has( code ) ) {
+                               collection._addedIncrement += 1;
+                               collection._addedOrder[ code ] = collection._addedIncrement;
+                       }
+                       return api.Values.prototype.add.call( this, code, params );
+               },
+
+               /**
+                * Add notification to the collection.
+                *
+                * @since 4.9.0
+                * @param {string} code - Notification code to remove.
+                * @return {api.Notification} Added instance (or existing instance if it was already added).
+                */
+               remove: function( code ) {
+                       var collection = this;
+                       delete collection._addedOrder[ code ];
+                       return api.Values.prototype.remove.call( this, code );
+               },
+
+               /**
+                * Get list of notifications.
+                *
+                * Notifications may be sorted by type followed by added time.
+                *
+                * @since 4.9.0
+                * @param {object}  args - Args.
+                * @param {boolean} [args.sort=false] - Whether to return the notifications sorted.
+                * @return {Array.<wp.customize.Notification>} Notifications.
+                * @this {wp.customize.Notifications}
+                */
+               get: function( args ) {
+                       var collection = this, notifications, errorTypePriorities, params;
+                       notifications = _.values( collection._value );
+
+                       params = _.extend(
+                               { sort: false },
+                               args
+                       );
+
+                       if ( params.sort ) {
+                               errorTypePriorities = { error: 4, warning: 3, success: 2, info: 1 };
+                               notifications.sort( function( a, b ) {
+                                       var aPriority = 0, bPriority = 0;
+                                       if ( ! _.isUndefined( errorTypePriorities[ a.type ] ) ) {
+                                               aPriority = errorTypePriorities[ a.type ];
+                                       }
+                                       if ( ! _.isUndefined( errorTypePriorities[ b.type ] ) ) {
+                                               bPriority = errorTypePriorities[ b.type ];
+                                       }
+                                       if ( aPriority !== bPriority ) {
+                                               return bPriority - aPriority; // Show errors first.
+                                       }
+                                       return collection._addedOrder[ b.code ] - collection._addedOrder[ a.code ]; // Show newer notifications higher.
+                               });
+                       }
+
+                       return notifications;
+               },
+
+               /**
+                * Render notifications area.
+                *
+                * @since 4.9.0
+                * @returns {void}
+                * @this {wp.customize.Notifications}
+                */
+               render: function() {
+                       var collection = this,
+                               notifications,
+                               renderedNotificationContainers,
+                               prevRenderedCodes,
+                               nextRenderedCodes,
+                               addedCodes,
+                               removedCodes,
+                               listElement;
+
+                       // Short-circuit if there are no container to render into.
+                       if ( ! collection.container || ! collection.container.length ) {
+                               return;
+                       }
+                       listElement = collection.container.children( 'ul' ).first();
+                       if ( ! listElement.length ) {
+                               listElement = $( '<ul></ul>' );
+                               collection.container.append( listElement );
+                       }
+
+                       notifications = collection.get( { sort: true } );
+
+                       renderedNotificationContainers = {};
+                       listElement.find( '> [data-code]' ).each( function() {
+                               renderedNotificationContainers[ $( this ).data( 'code' ) ] = $( this );
+                       });
+
+                       collection.container.toggle( 0 !== notifications.length );
+
+                       nextRenderedCodes = _.pluck( notifications, 'code' );
+                       prevRenderedCodes = _.keys( renderedNotificationContainers );
+
+                       // Short-circuit if there are no notifications added.
+                       if ( _.isEqual( nextRenderedCodes, prevRenderedCodes ) ) {
+                               return;
+                       }
+
+                       addedCodes = _.difference( nextRenderedCodes, prevRenderedCodes );
+                       removedCodes = _.difference( prevRenderedCodes, nextRenderedCodes );
+
+                       // Remove notifications that have been removed.
+                       _.each( renderedNotificationContainers, function( renderedContainer, code ) {
+                               if ( -1 !== _.indexOf( removedCodes, code ) ) {
+                                       renderedContainer.remove(); // @todo Consider slideUp as enhancement.
+                               }
+                       });
+
+                       // Add all notifications in the sorted order.
+                       _.each( notifications, function( notification ) {
+                               var notificationContainer = renderedNotificationContainers[ notification.code ];
+                               if ( notificationContainer ) {
+                                       listElement.append( notificationContainer );
+                               } else {
+                                       notificationContainer = $( notification.render() );
+                                       listElement.append( notificationContainer ); // @todo Consider slideDown() as enhancement.
+                                       if ( wp.a11y ) {
+                                               wp.a11y.speak( notification.message, 'assertive' );
+                                       }
+                               }
+                       });
+
+                       collection.trigger( 'rendered' );
+               }
+       });
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * A Customizer Setting.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * A setting is WordPress data (theme mod, option, menu, etc.) that the user can
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1883,7 +2089,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        control.priority = new api.Value();
</span><span class="cx" style="display: block; padding: 0 10px">                        control.active = new api.Value();
</span><span class="cx" style="display: block; padding: 0 10px">                        control.activeArgumentsQueue = [];
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        control.notifications = new api.Values({ defaultConstructor: api.Notification });
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 control.notifications = new api.Notifications({
+                               alt: control.altNotice
+                       });
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        control.elements = [];
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1973,21 +2181,17 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        // After the control is embedded on the page, invoke the "ready" method.
</span><span class="cx" style="display: block; padding: 0 10px">                        control.deferred.embedded.done( function () {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                /*
-                                * Note that this debounced/deferred rendering is needed for two reasons:
-                                * 1) The 'remove' event is triggered just _before_ the notification is actually removed.
-                                * 2) Improve performance when adding/removing multiple notifications at a time.
-                                */
-                               var debouncedRenderNotifications = _.debounce( function renderNotifications() {
-                                       control.renderNotifications();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         var renderNotifications = function() {
+                                       control.notifications.render();
+                               };
+                               control.notifications.container = control.getNotificationsContainerElement();
+                               control.notifications.bind( 'rendered', function() {
+                                       var notifications = control.notifications.get();
+                                       control.container.toggleClass( 'has-notifications', 0 !== notifications.length );
+                                       control.container.toggleClass( 'has-error', 0 !== _.where( notifications, { type: 'error' } ).length );
</ins><span class="cx" style="display: block; padding: 0 10px">                                 } );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                control.notifications.bind( 'add', function( notification ) {
-                                       wp.a11y.speak( notification.message, 'assertive' );
-                                       debouncedRenderNotifications();
-                               } );
-                               control.notifications.bind( 'remove', debouncedRenderNotifications );
-                               control.renderNotifications();
-
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         renderNotifications();
+                               control.notifications.bind( 'change', _.debounce( renderNotifications ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                                 control.ready();
</span><span class="cx" style="display: block; padding: 0 10px">                        });
</span><span class="cx" style="display: block; padding: 0 10px">                },
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2091,11 +2295,17 @@
</span><span class="cx" style="display: block; padding: 0 10px">                 * Control subclasses may override this method to do their own handling
</span><span class="cx" style="display: block; padding: 0 10px">                 * of rendering notifications.
</span><span class="cx" style="display: block; padding: 0 10px">                 *
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 * @deprecated in favor of `control.notifications.render()`
</ins><span class="cx" style="display: block; padding: 0 10px">                  * @since 4.6.0
</span><span class="cx" style="display: block; padding: 0 10px">                 * @this {wp.customize.Control}
</span><span class="cx" style="display: block; padding: 0 10px">                 */
</span><span class="cx" style="display: block; padding: 0 10px">                renderNotifications: function() {
</span><span class="cx" style="display: block; padding: 0 10px">                        var control = this, container, notifications, hasError = false;
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+                       if ( 'undefined' !== typeof console && console.warn ) {
+                               console.warn( '[DEPRECATED] wp.customize.Control.prototype.renderNotifications() is deprecated in favor of instantating a wp.customize.Notifications and calling its render() method.' );
+                       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                         container = control.getNotificationsContainerElement();
</span><span class="cx" style="display: block; padding: 0 10px">                        if ( ! container || ! container.length ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                return;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3427,6 +3637,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">        api.section = new api.Values({ defaultConstructor: api.Section });
</span><span class="cx" style="display: block; padding: 0 10px">        api.panel = new api.Values({ defaultConstructor: api.Panel });
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        // Create the collection for global Notifications.
+       api.notifications = new api.Notifications();
+
</ins><span class="cx" style="display: block; padding: 0 10px">         /**
</span><span class="cx" style="display: block; padding: 0 10px">         * An object that fetches a preview in the background of the document, which
</span><span class="cx" style="display: block; padding: 0 10px">         * allows for seamless replacement of an existing preview.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4501,6 +4714,13 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                                api.unbind( 'change', captureSettingModifiedDuringSave );
</span><span class="cx" style="display: block; padding: 0 10px">                                        } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                        // Remove notifications that were added due to save failures.
+                                       api.notifications.each( function( notification ) {
+                                               if ( notification.saveFailure ) {
+                                                       api.notifications.remove( notification.code );
+                                               }
+                                       });
+
</ins><span class="cx" style="display: block; padding: 0 10px">                                         request.fail( function ( response ) {
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                                if ( '0' === response ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4518,6 +4738,22 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                                                previewer.save();
</span><span class="cx" style="display: block; padding: 0 10px">                                                                previewer.preview.iframe.show();
</span><span class="cx" style="display: block; padding: 0 10px">                                                        } );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                                } else if ( response.code ) {
+                                                       api.notifications.add( response.code, new api.Notification( response.code, {
+                                                               message: response.message,
+                                                               type: 'error',
+                                                               dismissible: true,
+                                                               fromServer: true,
+                                                               saveFailure: true
+                                                       } ) );
+                                               } else {
+                                                       api.notifications.add( 'unknown_error', new api.Notification( 'unknown_error', {
+                                                               message: api.l10n.serverSaveError,
+                                                               type: 'error',
+                                                               dismissible: true,
+                                                               fromServer: true,
+                                                               saveFailure: true
+                                                       } ) );
</ins><span class="cx" style="display: block; padding: 0 10px">                                                 }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                                if ( response.setting_validities ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4688,6 +4924,29 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        values.bind( 'remove', debouncedReflowPaneContents );
</span><span class="cx" style="display: block; padding: 0 10px">                } );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                // Set up global notifications area.
+               api.bind( 'ready', function setUpGlobalNotificationsArea() {
+                       var sidebar, containerHeight, containerInitialTop;
+                       api.notifications.container = $( '#customize-notifications-area' );
+
+                       api.notifications.bind( 'change', _.debounce( function() {
+                               api.notifications.render();
+                       } ) );
+
+                       sidebar = $( '.wp-full-overlay-sidebar-content' );
+                       api.notifications.bind( 'rendered', function updateSidebarTop() {
+                               sidebar.css( 'top', '' );
+                               if ( 0 !== api.notifications.count() ) {
+                                       containerHeight = api.notifications.container.outerHeight() + 1;
+                                       containerInitialTop = parseInt( sidebar.css( 'top' ), 10 );
+                                       sidebar.css( 'top', containerInitialTop + containerHeight + 'px' );
+                               }
+                               api.notifications.trigger( 'sidebarTopUpdated' );
+                       });
+
+                       api.notifications.render();
+               });
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 // Save and activated states
</span><span class="cx" style="display: block; padding: 0 10px">                (function() {
</span><span class="cx" style="display: block; padding: 0 10px">                        var state = new api.Values(),
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4971,12 +5230,32 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                var scrollTop = parentContainer.scrollTop(),
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        isScrollingUp = ( lastScrollTop ) ? scrollTop <= lastScrollTop : true;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 scrollDirection;
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                if ( ! lastScrollTop ) {
+                                       scrollDirection = 1;
+                               } else {
+                                       if ( scrollTop === lastScrollTop ) {
+                                               scrollDirection = 0;
+                                       } else if ( scrollTop > lastScrollTop ) {
+                                               scrollDirection = 1;
+                                       } else {
+                                               scrollDirection = -1;
+                                       }
+                               }
</ins><span class="cx" style="display: block; padding: 0 10px">                                 lastScrollTop = scrollTop;
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                positionStickyHeader( activeHeader, scrollTop, isScrollingUp );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         if ( 0 !== scrollDirection ) {
+                                       positionStickyHeader( activeHeader, scrollTop, scrollDirection );
+                               }
</ins><span class="cx" style="display: block; padding: 0 10px">                         }, 8 ) );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        // Update header position on sidebar layout change.
+                       api.notifications.bind( 'sidebarTopUpdated', function() {
+                               if ( activeHeader && activeHeader.element.hasClass( 'is-sticky' ) ) {
+                                       activeHeader.element.css( 'top', parentContainer.css( 'top' ) );
+                               }
+                       });
+
</ins><span class="cx" style="display: block; padding: 0 10px">                         // Release header element if it is sticky.
</span><span class="cx" style="display: block; padding: 0 10px">                        releaseStickyHeader = function( headerElement ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                if ( ! headerElement.hasClass( 'is-sticky' ) ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -4990,13 +5269,15 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        // Reset position of the sticky header.
</span><span class="cx" style="display: block; padding: 0 10px">                        resetStickyHeader = function( headerElement, headerParent ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                headerElement
-                                       .removeClass( 'maybe-sticky is-in-view' )
-                                       .css( {
-                                               width: '',
-                                               top: ''
-                                       } );
-                               headerParent.css( 'padding-top', '' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         if ( headerElement.hasClass( 'is-in-view' ) ) {
+                                       headerElement
+                                               .removeClass( 'maybe-sticky is-in-view' )
+                                               .css( {
+                                                       width: '',
+                                                       top:   ''
+                                               } );
+                                       headerParent.css( 'padding-top', '' );
+                               }
</ins><span class="cx" style="display: block; padding: 0 10px">                         };
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -5023,19 +5304,20 @@
</span><span class="cx" style="display: block; padding: 0 10px">                         * @since 4.7.0
</span><span class="cx" style="display: block; padding: 0 10px">                         * @access private
</span><span class="cx" style="display: block; padding: 0 10px">                         *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                         * @param {object}  header        Header.
-                        * @param {number}  scrollTop     Scroll top.
-                        * @param {boolean} isScrollingUp Is scrolling up?
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                  * @param {object} header - Header.
+                        * @param {number} scrollTop - Scroll top.
+                        * @param {number} scrollDirection - Scroll direction, negative number being up and positive being down.
</ins><span class="cx" style="display: block; padding: 0 10px">                          * @returns {void}
</span><span class="cx" style="display: block; padding: 0 10px">                         */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        positionStickyHeader = function( header, scrollTop, isScrollingUp ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 positionStickyHeader = function( header, scrollTop, scrollDirection ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                                 var headerElement = header.element,
</span><span class="cx" style="display: block; padding: 0 10px">                                        headerParent = header.parent,
</span><span class="cx" style="display: block; padding: 0 10px">                                        headerHeight = header.height,
</span><span class="cx" style="display: block; padding: 0 10px">                                        headerTop = parseInt( headerElement.css( 'top' ), 10 ),
</span><span class="cx" style="display: block; padding: 0 10px">                                        maybeSticky = headerElement.hasClass( 'maybe-sticky' ),
</span><span class="cx" style="display: block; padding: 0 10px">                                        isSticky = headerElement.hasClass( 'is-sticky' ),
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        isInView = headerElement.hasClass( 'is-in-view' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 isInView = headerElement.hasClass( 'is-in-view' ),
+                                       isScrollingUp = ( -1 === scrollDirection );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                // When scrolling down, gradually hide sticky header.
</span><span class="cx" style="display: block; padding: 0 10px">                                if ( ! isScrollingUp ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -5078,7 +5360,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                                headerElement
</span><span class="cx" style="display: block; padding: 0 10px">                                                        .addClass( 'is-sticky' )
</span><span class="cx" style="display: block; padding: 0 10px">                                                        .css( {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                                                top:   '',
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                                         top:   parentContainer.css( 'top' ),
</ins><span class="cx" style="display: block; padding: 0 10px">                                                                 width: headerParent.outerWidth() + 'px'
</span><span class="cx" style="display: block; padding: 0 10px">                                                        } );
</span><span class="cx" style="display: block; padding: 0 10px">                                        }
</span></span></pre></div>
<a id="trunksrcwpadminjscustomizewidgetsjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-admin/js/customize-widgets.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-admin/js/customize-widgets.js        2017-09-12 03:33:58 UTC (rev 41373)
+++ trunk/src/wp-admin/js/customize-widgets.js  2017-09-12 07:02:49 UTC (rev 41374)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -550,6 +550,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px">                        control.widgetContentEmbedded = true;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        // Update the notification container element now that the widget content has been embedded.
+                       control.notifications.container = control.getNotificationsContainerElement();
+                       control.notifications.render();
+
</ins><span class="cx" style="display: block; padding: 0 10px">                         widgetContent = $( control.params.widget_content );
</span><span class="cx" style="display: block; padding: 0 10px">                        control.container.find( '.widget-content:first' ).append( widgetContent );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span></span></pre></div>
<a id="trunksrcwpincludesclasswpcustomizemanagerphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/class-wp-customize-manager.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-customize-manager.php      2017-09-12 03:33:58 UTC (rev 41373)
+++ trunk/src/wp-includes/class-wp-customize-manager.php        2017-09-12 07:02:49 UTC (rev 41374)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -348,7 +348,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                add_action( 'customize_controls_init',            array( $this, 'prepare_controls' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_control_scripts' ) );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // Render Panel, Section, and Control templates.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         // Render Common, Panel, Section, and Control templates.
</ins><span class="cx" style="display: block; padding: 0 10px">                 add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_panel_templates' ), 1 );
</span><span class="cx" style="display: block; padding: 0 10px">                add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_section_templates' ), 1 );
</span><span class="cx" style="display: block; padding: 0 10px">                add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_control_templates' ), 1 );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2355,7 +2355,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                if ( $update_transactionally && $invalid_setting_count > 0 ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        $response = array(
</span><span class="cx" style="display: block; padding: 0 10px">                                'setting_validities' => $setting_validities,
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                'message' => sprintf( _n( 'There is %s invalid setting.', 'There are %s invalid settings.', $invalid_setting_count ), number_format_i18n( $invalid_setting_count ) ),
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         /* translators: placeholder is number of invalid settings */
+                               '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 ) ),
</ins><span class="cx" style="display: block; padding: 0 10px">                         );
</span><span class="cx" style="display: block; padding: 0 10px">                        return new WP_Error( 'transaction_fail', '', $response );
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3183,7 +3184,20 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        ) );
</span><span class="cx" style="display: block; padding: 0 10px">                        $control->print_template();
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
</ins><span class="cx" style="display: block; padding: 0 10px">                 ?>
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                <script type="text/html" id="tmpl-customize-notification">
+                       <li class="notice notice-{{ data.type || 'info' }} {{ data.alt ? 'notice-alt' : '' }} {{ data.dismissible ? 'is-dismissible' : '' }}" data-code="{{ data.code }}" data-type="{{ data.type }}">
+                               {{{ data.message || data.code }}}
+                               <# if ( data.dismissible ) { #>
+                                       <button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php _e( 'Dismiss' ); ?></span></button>
+                               <# } #>
+                       </li>
+               </script>
+
+               <?php
+               /* The following template is obsolete in core but retained for plugins. */
+               ?>
</ins><span class="cx" style="display: block; padding: 0 10px">                 <script type="text/html" id="tmpl-customize-control-notifications">
</span><span class="cx" style="display: block; padding: 0 10px">                        <ul>
</span><span class="cx" style="display: block; padding: 0 10px">                                <# _.each( data.notifications, function( notification ) { #>
</span></span></pre></div>
<a id="trunksrcwpincludesjscustomizebasejs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/js/customize-base.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/js/customize-base.js        2017-09-12 03:33:58 UTC (rev 41373)
+++ trunk/src/wp-includes/js/customize-base.js  2017-09-12 07:02:49 UTC (rev 41374)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -433,18 +433,26 @@
</span><span class="cx" style="display: block; padding: 0 10px">                 * @param  {string} id The ID of the item to remove.
</span><span class="cx" style="display: block; padding: 0 10px">                 */
</span><span class="cx" style="display: block; padding: 0 10px">                remove: function( id ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        var value;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 var value = this.value( id );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        if ( this.has( id ) ) {
-                               value = this.value( id );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 if ( value ) {
+
+                               // Trigger event right before the element is removed from the collection.
</ins><span class="cx" style="display: block; padding: 0 10px">                                 this.trigger( 'remove', value );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                if ( value.extended( api.Value ) )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+                               if ( value.extended( api.Value ) ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                                         value.unbind( this._change );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                }
</ins><span class="cx" style="display: block; padding: 0 10px">                                 delete value.parent;
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        delete this._value[ id ];
</span><span class="cx" style="display: block; padding: 0 10px">                        delete this._deferreds[ id ];
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+                       // Trigger removed event after the item has been eliminated from the collection.
+                       if ( value ) {
+                               this.trigger( 'removed', value );
+                       }
</ins><span class="cx" style="display: block; padding: 0 10px">                 },
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -790,6 +798,39 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @param {*}       [params.data=null] - Any additional data.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        api.Notification = api.Class.extend(/** @lends wp.customize.Notification.prototype */{
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+               /**
+                * Template function for rendering the notification.
+                *
+                * This will be populated with template option or else it will be populated with template from the ID.
+                *
+                * @since 4.9.0
+                * @var {Function}
+                */
+               template: null,
+
+               /**
+                * ID for the template to render the notification.
+                *
+                * @since 4.9.0
+                * @var {string}
+                */
+               templateId: 'customize-notification',
+
+               /**
+                * Initialize notification.
+                *
+                * @since 4.9.0
+                *
+                * @param {string}   code - Notification code.
+                * @param {object}   params - Notification parameters.
+                * @param {string}   params.message - Message.
+                * @param {string}   [params.type=error] - Type.
+                * @param {string}   [params.setting] - Related setting ID.
+                * @param {Function} [params.template] - Function for rendering template. If not provided, this will come from templateId.
+                * @param {string}   [params.templateId] - ID for template to render the notification.
+                * @param {boolean}  [params.dismissible] - Whether the notification can be dismissed.
+                */
</ins><span class="cx" style="display: block; padding: 0 10px">                 initialize: function( code, params ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        var _params;
</span><span class="cx" style="display: block; padding: 0 10px">                        this.code = code;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -799,12 +840,44 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                        type: 'error',
</span><span class="cx" style="display: block; padding: 0 10px">                                        fromServer: false,
</span><span class="cx" style="display: block; padding: 0 10px">                                        data: null,
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        setting: null
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 setting: null,
+                                       template: null,
+                                       dismissible: false
</ins><span class="cx" style="display: block; padding: 0 10px">                                 },
</span><span class="cx" style="display: block; padding: 0 10px">                                params
</span><span class="cx" style="display: block; padding: 0 10px">                        );
</span><span class="cx" style="display: block; padding: 0 10px">                        delete _params.code;
</span><span class="cx" style="display: block; padding: 0 10px">                        _.extend( this, _params );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                },
+
+               /**
+                * Render the notification.
+                *
+                * @since 4.9.0
+                *
+                * @returns {jQuery} Notification container element.
+                */
+               render: function() {
+                       var notification = this, container, data;
+                       if ( ! notification.template ) {
+                               notification.template = wp.template( notification.templateId );
+                       }
+                       data = _.extend( {}, notification, {
+                               alt: notification.parent && notification.parent.alt
+                       } );
+                       container = $( notification.template( data ) );
+
+                       if ( notification.dismissible ) {
+                               container.find( '.notice-dismiss' ).on( 'click', function() {
+                                       if ( notification.parent ) {
+                                               notification.parent.remove( notification.code );
+                                       } else {
+                                               container.remove();
+                                       }
+                               });
+                       }
+
+                       return container;
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><span class="cx" style="display: block; padding: 0 10px">        });
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span></span></pre></div>
<a id="trunksrcwpincludesscriptloaderphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/script-loader.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/script-loader.php   2017-09-12 03:33:58 UTC (rev 41373)
+++ trunk/src/wp-includes/script-loader.php     2017-09-12 07:02:49 UTC (rev 41374)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -546,6 +546,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                'collapseSidebar'    => _x( 'Hide Controls', 'label for hide controls button without length constraints' ),
</span><span class="cx" style="display: block; padding: 0 10px">                'expandSidebar'      => _x( 'Show Controls', 'label for hide controls button without length constraints' ),
</span><span class="cx" style="display: block; padding: 0 10px">                'untitledBlogName'   => __( '(Untitled)' ),
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                'serverSaveError'    => __( 'Failed connecting to the server. Please try saving again.' ),
</ins><span class="cx" style="display: block; padding: 0 10px">                 // Used for overriding the file types allowed in plupload.
</span><span class="cx" style="display: block; padding: 0 10px">                'allowedFiles'       => __( 'Allowed Files' ),
</span><span class="cx" style="display: block; padding: 0 10px">        ) );
</span></span></pre></div>
<a id="trunktestsqunitindexhtml"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/qunit/index.html</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/qunit/index.html      2017-09-12 03:33:58 UTC (rev 41373)
+++ trunk/tests/qunit/index.html        2017-09-12 07:02:49 UTC (rev 41374)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -210,6 +210,14 @@
</span><span class="cx" style="display: block; padding: 0 10px">                                <# } ); #>
</span><span class="cx" style="display: block; padding: 0 10px">                        </ul>
</span><span class="cx" style="display: block; padding: 0 10px">                </script>
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                <script type="text/html" id="tmpl-customize-notification">
+                       <li class="notice notice-{{ data.type || 'info' }} {{ data.altNotice ? 'notice-alt' : '' }} {{ data.dismissible ? 'is-dismissible' : '' }}" data-code="{{ data.code }}" data-type="{{ data.type }}">
+                               {{{ data.message || data.code }}}
+                               <# if ( data.dismissible ) { #>
+                                       <button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php _e( 'Dismiss' ); ?></span></button>
+                               <# } #>
+                       </li>
+               </script>
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                <!-- Templates for Customizer Menus -->
</span><span class="cx" style="display: block; padding: 0 10px">                <script type="text/html" id="tmpl-customize-control-nav_menu-content">
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -386,6 +394,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                <div hidden>
</span><span class="cx" style="display: block; padding: 0 10px">                        <div id="customize-preview"></div>
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        <div id="customize-notifications-test"><ul></ul></div>
</ins><span class="cx" style="display: block; padding: 0 10px">                 </div>
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                <div hidden>
</span></span></pre></div>
<a id="trunktestsqunitwpadminjscustomizebasejs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/qunit/wp-admin/js/customize-base.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/qunit/wp-admin/js/customize-base.js   2017-09-12 03:33:58 UTC (rev 41373)
+++ trunk/tests/qunit/wp-admin/js/customize-base.js     2017-09-12 07:02:49 UTC (rev 41374)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2,7 +2,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> jQuery( function( $ ) {
</span><span class="cx" style="display: block; padding: 0 10px">        var FooSuperClass, BarSubClass, foo, bar, ConstructorTestClass, newConstructor, constructorTest, $mockElement, mockString,
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        firstInitialValue, firstValueInstance, wasCallbackFired, mockValueCallback;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ firstInitialValue, firstValueInstance, valuesInstance, wasCallbackFired, mockValueCallback;
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        module( 'Customize Base: Class' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -159,6 +159,52 @@
</span><span class="cx" style="display: block; padding: 0 10px">                ok( wasCallbackFired );
</span><span class="cx" style="display: block; padding: 0 10px">        });
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        module( 'Customize Base: Values Class' );
+
+       valuesInstance = new wp.customize.Values();
+
+       test( 'Correct events are triggered when adding to or removing from Values collection', function() {
+               var hasFooOnAdd = false,
+                       hasFooOnRemove = false,
+                       hasFooOnRemoved = true,
+                       valuePassedToAdd = false,
+                       valuePassedToRemove = false,
+                       valuePassedToRemoved = false,
+                       wasEventFiredOnRemoval = false,
+                       fooValue = new wp.customize.Value( 'foo' );
+
+               // Test events when adding new value.
+               valuesInstance.bind( 'add', function( value ) {
+                       hasFooOnAdd = valuesInstance.has( 'foo' );
+                       valuePassedToAdd = value;
+               } );
+               valuesInstance.add( 'foo', fooValue );
+               ok( hasFooOnAdd );
+               equal( valuePassedToAdd.get(), fooValue.get() );
+
+               // Test events when removing the value.
+               valuesInstance.bind( 'remove', function( value ) {
+                       hasFooOnRemove = valuesInstance.has( 'foo' );
+                       valuePassedToRemove = value;
+                       wasEventFiredOnRemoval = true;
+               } );
+               valuesInstance.bind( 'removed', function( value ) {
+                       hasFooOnRemoved = valuesInstance.has( 'foo' );
+                       valuePassedToRemoved = value;
+                       wasEventFiredOnRemoval = true;
+               } );
+               valuesInstance.remove( 'foo' );
+               ok( hasFooOnRemove );
+               equal( valuePassedToRemove.get(), fooValue.get() );
+               ok( ! hasFooOnRemoved );
+               equal( valuePassedToRemoved.get(), fooValue.get() );
+
+               // Confirm no events are fired when nonexistent value is removed.
+               wasEventFiredOnRemoval = false;
+               valuesInstance.remove( 'bar' );
+               ok( ! wasEventFiredOnRemoval );
+       });
+
</ins><span class="cx" style="display: block; padding: 0 10px">         module( 'Customize Base: Notification' );
</span><span class="cx" style="display: block; padding: 0 10px">        test( 'Notification object exists and has expected properties', function ( assert ) {
</span><span class="cx" style="display: block; padding: 0 10px">                var notification = new wp.customize.Notification( 'mycode', {
</span></span></pre></div>
<a id="trunktestsqunitwpadminjscustomizecontrolsjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/qunit/wp-admin/js/customize-controls.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/qunit/wp-admin/js/customize-controls.js       2017-09-12 03:33:58 UTC (rev 41373)
+++ trunk/tests/qunit/wp-admin/js/customize-controls.js 2017-09-12 07:02:49 UTC (rev 41374)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -82,6 +82,47 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        };
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        module( 'Customizer notifications collection' );
+       test( 'Notifications collection exists', function() {
+               ok( wp.customize.notifications );
+               equal( wp.customize.notifications.defaultConstructor, wp.customize.Notification );
+       } );
+
+       test( 'Notification objects are rendered as part of notifications collection', function() {
+               var container = jQuery( '#customize-notifications-test' ), items, collection;
+
+               collection = new wp.customize.Notifications({
+                       container: container
+               });
+               collection.add( 'mycode-1', new wp.customize.Notification( 'mycode-1' ) );
+               collection.render();
+               items = collection.container.find( 'li' );
+               equal( items.length, 1 );
+               equal( items.first().data( 'code' ), 'mycode-1' );
+
+               collection.add( 'mycode-2', new wp.customize.Notification( 'mycode-2', {
+                       dismissible: true
+               } ) );
+               collection.render();
+               items = collection.container.find( 'li' );
+               equal( items.length, 2 );
+               equal( items.first().data( 'code' ), 'mycode-2' );
+               equal( items.last().data( 'code' ), 'mycode-1' );
+
+               equal( items.first().find( '.notice-dismiss' ).length, 1 );
+               equal( items.last().find( '.notice-dismiss' ).length, 0 );
+
+               collection.remove( 'mycode-2' );
+               collection.render();
+               items = collection.container.find( 'li' );
+               equal( items.length, 1 );
+               equal( items.first().data( 'code' ), 'mycode-1' );
+
+               collection.remove( 'mycode-1' );
+               collection.render();
+               ok( collection.container.is( ':hidden' ), 'Notifications area is hidden.' );
+       } );
+
</ins><span class="cx" style="display: block; padding: 0 10px">         module( 'Customizer Previewed Device' );
</span><span class="cx" style="display: block; padding: 0 10px">        test( 'Previewed device defaults to desktop.', function () {
</span><span class="cx" style="display: block; padding: 0 10px">                equal( wp.customize.previewedDevice.get(), 'desktop' );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -144,7 +185,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        assert.equal( 1, notificationContainerElement.length );
</span><span class="cx" style="display: block; padding: 0 10px">                        assert.ok( notificationContainerElement.is( '.customize-control-notifications-container' ) );
</span><span class="cx" style="display: block; padding: 0 10px">                        assert.equal( 0, notificationContainerElement.find( '> ul > li' ).length );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        assert.equal( 'none', notificationContainerElement.css( 'display' ) );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 assert.equal( 0, notificationContainerElement.height() );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        settingNotification = new wp.customize.Notification( 'setting_invalidity', 'Invalid setting' );
</span><span class="cx" style="display: block; padding: 0 10px">                        controlOnlyNotification = new wp.customize.Notification( 'control_invalidity', 'Invalid control' );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -152,7 +193,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        control.notifications.add( controlOnlyNotification.code, controlOnlyNotification );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        // Note that renderNotifications is being called manually here since rendering normally happens asynchronously.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        control.renderNotifications();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 control.notifications.render();
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        assert.equal( 2, notificationContainerElement.find( '> ul > li' ).length );
</span><span class="cx" style="display: block; padding: 0 10px">                        assert.notEqual( 'none', notificationContainerElement.css( 'display' ) );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -160,14 +201,13 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        assert.equal( 1, _.size( control.settings['default'].notifications._value ) );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        control.notifications.remove( controlOnlyNotification.code );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        control.renderNotifications();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 control.notifications.render();
</ins><span class="cx" style="display: block; padding: 0 10px">                         assert.equal( 1, notificationContainerElement.find( '> ul > li' ).length );
</span><span class="cx" style="display: block; padding: 0 10px">                        assert.notEqual( 'none', notificationContainerElement.css( 'display' ) );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        control.settings['default'].notifications.remove( settingNotification.code );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        control.renderNotifications();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 control.notifications.render();
</ins><span class="cx" style="display: block; padding: 0 10px">                         assert.equal( 0, notificationContainerElement.find( '> ul > li' ).length );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        assert.ok( notificationContainerElement.is( ':animated' ) ); // It is being slid down.
</del><span class="cx" style="display: block; padding: 0 10px">                         notificationContainerElement.stop().hide(); // Clean up.
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        doneEmbedded();
</span></span></pre>
</div>
</div>

</body>
</html>