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.


git-svn-id: https://develop.svn.wordpress.org/trunk@41374 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
Weston Ruter 2017-09-12 07:02:49 +00:00
parent 84487618eb
commit 594a41666b
10 changed files with 557 additions and 59 deletions

View File

@ -766,7 +766,6 @@ p.customize-section-description {
#customize-controls .customize-control-notifications-container { /* Scoped to #customize-controls for specificity over notification styles in common.css. */
margin: 4px 0 8px 0;
padding: 0;
display: none;
cursor: default;
}
@ -798,6 +797,33 @@ p.customize-section-description {
outline: 2px solid #dc3232;
}
#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;
}
/* Style for custom settings */
/**

View File

@ -151,24 +151,27 @@ do_action( 'customize_controls_print_scripts' );
</div>
<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 -->
<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 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>
</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>
</div>
<div id="customize-theme-controls">
<ul class="customize-pane-parent"><?php // Panels and sections are managed here via JavaScript ?></ul>
<div id="customize-theme-controls">
<ul class="customize-pane-parent"><?php // Panels and sections are managed here via JavaScript ?></ul>
</div>
</div>
</div>
</div>
<div id="customize-footer-actions" class="wp-full-overlay-footer">
<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' ) ); ?>">

View File

@ -1,7 +1,213 @@
/* global _wpCustomizeHeader, _wpCustomizeBackground, _wpMediaViewsL10n, MediaElementPlayer */
/* global _wpCustomizeHeader, _wpCustomizeBackground, _wpMediaViewsL10n, MediaElementPlayer, console */
(function( exports, $ ){
var Container, focus, normalizedTransitionendEventName, api = wp.customize;
/**
* 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' );
}
});
/**
* A Customizer Setting.
*
@ -1883,7 +2089,9 @@
control.priority = new api.Value();
control.active = new api.Value();
control.activeArgumentsQueue = [];
control.notifications = new api.Values({ defaultConstructor: api.Notification });
control.notifications = new api.Notifications({
alt: control.altNotice
});
control.elements = [];
@ -1973,21 +2181,17 @@
// After the control is embedded on the page, invoke the "ready" method.
control.deferred.embedded.done( function () {
/*
* 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();
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 );
} );
control.notifications.bind( 'add', function( notification ) {
wp.a11y.speak( notification.message, 'assertive' );
debouncedRenderNotifications();
} );
control.notifications.bind( 'remove', debouncedRenderNotifications );
control.renderNotifications();
renderNotifications();
control.notifications.bind( 'change', _.debounce( renderNotifications ) );
control.ready();
});
},
@ -2091,11 +2295,17 @@
* Control subclasses may override this method to do their own handling
* of rendering notifications.
*
* @deprecated in favor of `control.notifications.render()`
* @since 4.6.0
* @this {wp.customize.Control}
*/
renderNotifications: function() {
var control = this, container, notifications, hasError = false;
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.' );
}
container = control.getNotificationsContainerElement();
if ( ! container || ! container.length ) {
return;
@ -3427,6 +3637,9 @@
api.section = new api.Values({ defaultConstructor: api.Section });
api.panel = new api.Values({ defaultConstructor: api.Panel });
// Create the collection for global Notifications.
api.notifications = new api.Notifications();
/**
* An object that fetches a preview in the background of the document, which
* allows for seamless replacement of an existing preview.
@ -4501,6 +4714,13 @@
api.unbind( 'change', captureSettingModifiedDuringSave );
} );
// Remove notifications that were added due to save failures.
api.notifications.each( function( notification ) {
if ( notification.saveFailure ) {
api.notifications.remove( notification.code );
}
});
request.fail( function ( response ) {
if ( '0' === response ) {
@ -4518,6 +4738,22 @@
previewer.save();
previewer.preview.iframe.show();
} );
} 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
} ) );
}
if ( response.setting_validities ) {
@ -4688,6 +4924,29 @@
values.bind( 'remove', debouncedReflowPaneContents );
} );
// 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();
});
// Save and activated states
(function() {
var state = new api.Values(),
@ -4971,12 +5230,32 @@
}
var scrollTop = parentContainer.scrollTop(),
isScrollingUp = ( lastScrollTop ) ? scrollTop <= lastScrollTop : true;
scrollDirection;
if ( ! lastScrollTop ) {
scrollDirection = 1;
} else {
if ( scrollTop === lastScrollTop ) {
scrollDirection = 0;
} else if ( scrollTop > lastScrollTop ) {
scrollDirection = 1;
} else {
scrollDirection = -1;
}
}
lastScrollTop = scrollTop;
positionStickyHeader( activeHeader, scrollTop, isScrollingUp );
if ( 0 !== scrollDirection ) {
positionStickyHeader( activeHeader, scrollTop, scrollDirection );
}
}, 8 ) );
// 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' ) );
}
});
// Release header element if it is sticky.
releaseStickyHeader = function( headerElement ) {
if ( ! headerElement.hasClass( 'is-sticky' ) ) {
@ -4990,13 +5269,15 @@
// Reset position of the sticky header.
resetStickyHeader = function( headerElement, headerParent ) {
headerElement
.removeClass( 'maybe-sticky is-in-view' )
.css( {
width: '',
top: ''
} );
headerParent.css( 'padding-top', '' );
if ( headerElement.hasClass( 'is-in-view' ) ) {
headerElement
.removeClass( 'maybe-sticky is-in-view' )
.css( {
width: '',
top: ''
} );
headerParent.css( 'padding-top', '' );
}
};
/**
@ -5023,19 +5304,20 @@
* @since 4.7.0
* @access private
*
* @param {object} header Header.
* @param {number} scrollTop Scroll top.
* @param {boolean} isScrollingUp Is scrolling up?
* @param {object} header - Header.
* @param {number} scrollTop - Scroll top.
* @param {number} scrollDirection - Scroll direction, negative number being up and positive being down.
* @returns {void}
*/
positionStickyHeader = function( header, scrollTop, isScrollingUp ) {
positionStickyHeader = function( header, scrollTop, scrollDirection ) {
var headerElement = header.element,
headerParent = header.parent,
headerHeight = header.height,
headerTop = parseInt( headerElement.css( 'top' ), 10 ),
maybeSticky = headerElement.hasClass( 'maybe-sticky' ),
isSticky = headerElement.hasClass( 'is-sticky' ),
isInView = headerElement.hasClass( 'is-in-view' );
isInView = headerElement.hasClass( 'is-in-view' ),
isScrollingUp = ( -1 === scrollDirection );
// When scrolling down, gradually hide sticky header.
if ( ! isScrollingUp ) {
@ -5078,7 +5360,7 @@
headerElement
.addClass( 'is-sticky' )
.css( {
top: '',
top: parentContainer.css( 'top' ),
width: headerParent.outerWidth() + 'px'
} );
}

View File

@ -550,6 +550,10 @@
}
control.widgetContentEmbedded = true;
// Update the notification container element now that the widget content has been embedded.
control.notifications.container = control.getNotificationsContainerElement();
control.notifications.render();
widgetContent = $( control.params.widget_content );
control.container.find( '.widget-content:first' ).append( widgetContent );

View File

@ -348,7 +348,7 @@ final class WP_Customize_Manager {
add_action( 'customize_controls_init', array( $this, 'prepare_controls' ) );
add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_control_scripts' ) );
// Render Panel, Section, and Control templates.
// Render Common, Panel, Section, and Control templates.
add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_panel_templates' ), 1 );
add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_section_templates' ), 1 );
add_action( 'customize_controls_print_footer_scripts', array( $this, 'render_control_templates' ), 1 );
@ -2355,7 +2355,8 @@ final class WP_Customize_Manager {
if ( $update_transactionally && $invalid_setting_count > 0 ) {
$response = array(
'setting_validities' => $setting_validities,
'message' => sprintf( _n( 'There is %s invalid setting.', 'There are %s invalid settings.', $invalid_setting_count ), number_format_i18n( $invalid_setting_count ) ),
/* 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 ) ),
);
return new WP_Error( 'transaction_fail', '', $response );
}
@ -3183,6 +3184,19 @@ final class WP_Customize_Manager {
) );
$control->print_template();
}
?>
<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. */
?>
<script type="text/html" id="tmpl-customize-control-notifications">
<ul>

View File

@ -433,18 +433,26 @@ window.wp = window.wp || {};
* @param {string} id The ID of the item to remove.
*/
remove: function( id ) {
var value;
var value = this.value( id );
if ( this.has( id ) ) {
value = this.value( id );
if ( value ) {
// Trigger event right before the element is removed from the collection.
this.trigger( 'remove', value );
if ( value.extended( api.Value ) )
if ( value.extended( api.Value ) ) {
value.unbind( this._change );
}
delete value.parent;
}
delete this._value[ id ];
delete this._deferreds[ id ];
// Trigger removed event after the item has been eliminated from the collection.
if ( value ) {
this.trigger( 'removed', value );
}
},
/**
@ -790,6 +798,39 @@ window.wp = window.wp || {};
* @param {*} [params.data=null] - Any additional data.
*/
api.Notification = api.Class.extend(/** @lends wp.customize.Notification.prototype */{
/**
* 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.
*/
initialize: function( code, params ) {
var _params;
this.code = code;
@ -799,12 +840,44 @@ window.wp = window.wp || {};
type: 'error',
fromServer: false,
data: null,
setting: null
setting: null,
template: null,
dismissible: false
},
params
);
delete _params.code;
_.extend( this, _params );
},
/**
* 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;
}
});

View File

@ -546,6 +546,7 @@ function wp_default_scripts( &$scripts ) {
'collapseSidebar' => _x( 'Hide Controls', 'label for hide controls button without length constraints' ),
'expandSidebar' => _x( 'Show Controls', 'label for hide controls button without length constraints' ),
'untitledBlogName' => __( '(Untitled)' ),
'serverSaveError' => __( 'Failed connecting to the server. Please try saving again.' ),
// Used for overriding the file types allowed in plupload.
'allowedFiles' => __( 'Allowed Files' ),
) );

View File

@ -210,6 +210,14 @@
<# } ); #>
</ul>
</script>
<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>
<!-- Templates for Customizer Menus -->
<script type="text/html" id="tmpl-customize-control-nav_menu-content">
@ -386,6 +394,7 @@
<div hidden>
<div id="customize-preview"></div>
<div id="customize-notifications-test"><ul></ul></div>
</div>
<div hidden>

View File

@ -2,7 +2,7 @@
jQuery( function( $ ) {
var FooSuperClass, BarSubClass, foo, bar, ConstructorTestClass, newConstructor, constructorTest, $mockElement, mockString,
firstInitialValue, firstValueInstance, wasCallbackFired, mockValueCallback;
firstInitialValue, firstValueInstance, valuesInstance, wasCallbackFired, mockValueCallback;
module( 'Customize Base: Class' );
@ -159,6 +159,52 @@ jQuery( function( $ ) {
ok( wasCallbackFired );
});
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 );
});
module( 'Customize Base: Notification' );
test( 'Notification object exists and has expected properties', function ( assert ) {
var notification = new wp.customize.Notification( 'mycode', {

View File

@ -82,6 +82,47 @@ jQuery( window ).load( function (){
};
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.' );
} );
module( 'Customizer Previewed Device' );
test( 'Previewed device defaults to desktop.', function () {
equal( wp.customize.previewedDevice.get(), 'desktop' );
@ -144,7 +185,7 @@ jQuery( window ).load( function (){
assert.equal( 1, notificationContainerElement.length );
assert.ok( notificationContainerElement.is( '.customize-control-notifications-container' ) );
assert.equal( 0, notificationContainerElement.find( '> ul > li' ).length );
assert.equal( 'none', notificationContainerElement.css( 'display' ) );
assert.equal( 0, notificationContainerElement.height() );
settingNotification = new wp.customize.Notification( 'setting_invalidity', 'Invalid setting' );
controlOnlyNotification = new wp.customize.Notification( 'control_invalidity', 'Invalid control' );
@ -152,7 +193,7 @@ jQuery( window ).load( function (){
control.notifications.add( controlOnlyNotification.code, controlOnlyNotification );
// Note that renderNotifications is being called manually here since rendering normally happens asynchronously.
control.renderNotifications();
control.notifications.render();
assert.equal( 2, notificationContainerElement.find( '> ul > li' ).length );
assert.notEqual( 'none', notificationContainerElement.css( 'display' ) );
@ -160,14 +201,13 @@ jQuery( window ).load( function (){
assert.equal( 1, _.size( control.settings['default'].notifications._value ) );
control.notifications.remove( controlOnlyNotification.code );
control.renderNotifications();
control.notifications.render();
assert.equal( 1, notificationContainerElement.find( '> ul > li' ).length );
assert.notEqual( 'none', notificationContainerElement.css( 'display' ) );
control.settings['default'].notifications.remove( settingNotification.code );
control.renderNotifications();
control.notifications.render();
assert.equal( 0, notificationContainerElement.find( '> ul > li' ).length );
assert.ok( notificationContainerElement.is( ':animated' ) ); // It is being slid down.
notificationContainerElement.stop().hide(); // Clean up.
doneEmbedded();