ru-se.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

customize-controls.js (291564B)


      1 /**
      2  * @output wp-admin/js/customize-controls.js
      3  */
      4 
      5 /* global _wpCustomizeHeader, _wpCustomizeBackground, _wpMediaViewsL10n, MediaElementPlayer, console, confirm */
      6 (function( exports, $ ){
      7 	var Container, focus, normalizedTransitionendEventName, api = wp.customize;
      8 
      9 	api.OverlayNotification = api.Notification.extend(/** @lends wp.customize.OverlayNotification.prototype */{
     10 
     11 		/**
     12 		 * Whether the notification should show a loading spinner.
     13 		 *
     14 		 * @since 4.9.0
     15 		 * @var {boolean}
     16 		 */
     17 		loading: false,
     18 
     19 		/**
     20 		 * A notification that is displayed in a full-screen overlay.
     21 		 *
     22 		 * @constructs wp.customize.OverlayNotification
     23 		 * @augments   wp.customize.Notification
     24 		 *
     25 		 * @since 4.9.0
     26 		 *
     27 		 * @param {string} code - Code.
     28 		 * @param {Object} params - Params.
     29 		 */
     30 		initialize: function( code, params ) {
     31 			var notification = this;
     32 			api.Notification.prototype.initialize.call( notification, code, params );
     33 			notification.containerClasses += ' notification-overlay';
     34 			if ( notification.loading ) {
     35 				notification.containerClasses += ' notification-loading';
     36 			}
     37 		},
     38 
     39 		/**
     40 		 * Render notification.
     41 		 *
     42 		 * @since 4.9.0
     43 		 *
     44 		 * @return {jQuery} Notification container.
     45 		 */
     46 		render: function() {
     47 			var li = api.Notification.prototype.render.call( this );
     48 			li.on( 'keydown', _.bind( this.handleEscape, this ) );
     49 			return li;
     50 		},
     51 
     52 		/**
     53 		 * Stop propagation on escape key presses, but also dismiss notification if it is dismissible.
     54 		 *
     55 		 * @since 4.9.0
     56 		 *
     57 		 * @param {jQuery.Event} event - Event.
     58 		 * @return {void}
     59 		 */
     60 		handleEscape: function( event ) {
     61 			var notification = this;
     62 			if ( 27 === event.which ) {
     63 				event.stopPropagation();
     64 				if ( notification.dismissible && notification.parent ) {
     65 					notification.parent.remove( notification.code );
     66 				}
     67 			}
     68 		}
     69 	});
     70 
     71 	api.Notifications = api.Values.extend(/** @lends wp.customize.Notifications.prototype */{
     72 
     73 		/**
     74 		 * Whether the alternative style should be used.
     75 		 *
     76 		 * @since 4.9.0
     77 		 * @type {boolean}
     78 		 */
     79 		alt: false,
     80 
     81 		/**
     82 		 * The default constructor for items of the collection.
     83 		 *
     84 		 * @since 4.9.0
     85 		 * @type {object}
     86 		 */
     87 		defaultConstructor: api.Notification,
     88 
     89 		/**
     90 		 * A collection of observable notifications.
     91 		 *
     92 		 * @since 4.9.0
     93 		 *
     94 		 * @constructs wp.customize.Notifications
     95 		 * @augments   wp.customize.Values
     96 		 *
     97 		 * @param {Object}  options - Options.
     98 		 * @param {jQuery}  [options.container] - Container element for notifications. This can be injected later.
     99 		 * @param {boolean} [options.alt] - Whether alternative style should be used when rendering notifications.
    100 		 *
    101 		 * @return {void}
    102 		 */
    103 		initialize: function( options ) {
    104 			var collection = this;
    105 
    106 			api.Values.prototype.initialize.call( collection, options );
    107 
    108 			_.bindAll( collection, 'constrainFocus' );
    109 
    110 			// Keep track of the order in which the notifications were added for sorting purposes.
    111 			collection._addedIncrement = 0;
    112 			collection._addedOrder = {};
    113 
    114 			// Trigger change event when notification is added or removed.
    115 			collection.bind( 'add', function( notification ) {
    116 				collection.trigger( 'change', notification );
    117 			});
    118 			collection.bind( 'removed', function( notification ) {
    119 				collection.trigger( 'change', notification );
    120 			});
    121 		},
    122 
    123 		/**
    124 		 * Get the number of notifications added.
    125 		 *
    126 		 * @since 4.9.0
    127 		 * @return {number} Count of notifications.
    128 		 */
    129 		count: function() {
    130 			return _.size( this._value );
    131 		},
    132 
    133 		/**
    134 		 * Add notification to the collection.
    135 		 *
    136 		 * @since 4.9.0
    137 		 *
    138 		 * @param {string|wp.customize.Notification} notification - Notification object to add. Alternatively code may be supplied, and in that case the second notificationObject argument must be supplied.
    139 		 * @param {wp.customize.Notification} [notificationObject] - Notification to add when first argument is the code string.
    140 		 * @return {wp.customize.Notification} Added notification (or existing instance if it was already added).
    141 		 */
    142 		add: function( notification, notificationObject ) {
    143 			var collection = this, code, instance;
    144 			if ( 'string' === typeof notification ) {
    145 				code = notification;
    146 				instance = notificationObject;
    147 			} else {
    148 				code = notification.code;
    149 				instance = notification;
    150 			}
    151 			if ( ! collection.has( code ) ) {
    152 				collection._addedIncrement += 1;
    153 				collection._addedOrder[ code ] = collection._addedIncrement;
    154 			}
    155 			return api.Values.prototype.add.call( collection, code, instance );
    156 		},
    157 
    158 		/**
    159 		 * Add notification to the collection.
    160 		 *
    161 		 * @since 4.9.0
    162 		 * @param {string} code - Notification code to remove.
    163 		 * @return {api.Notification} Added instance (or existing instance if it was already added).
    164 		 */
    165 		remove: function( code ) {
    166 			var collection = this;
    167 			delete collection._addedOrder[ code ];
    168 			return api.Values.prototype.remove.call( this, code );
    169 		},
    170 
    171 		/**
    172 		 * Get list of notifications.
    173 		 *
    174 		 * Notifications may be sorted by type followed by added time.
    175 		 *
    176 		 * @since 4.9.0
    177 		 * @param {Object}  args - Args.
    178 		 * @param {boolean} [args.sort=false] - Whether to return the notifications sorted.
    179 		 * @return {Array.<wp.customize.Notification>} Notifications.
    180 		 */
    181 		get: function( args ) {
    182 			var collection = this, notifications, errorTypePriorities, params;
    183 			notifications = _.values( collection._value );
    184 
    185 			params = _.extend(
    186 				{ sort: false },
    187 				args
    188 			);
    189 
    190 			if ( params.sort ) {
    191 				errorTypePriorities = { error: 4, warning: 3, success: 2, info: 1 };
    192 				notifications.sort( function( a, b ) {
    193 					var aPriority = 0, bPriority = 0;
    194 					if ( ! _.isUndefined( errorTypePriorities[ a.type ] ) ) {
    195 						aPriority = errorTypePriorities[ a.type ];
    196 					}
    197 					if ( ! _.isUndefined( errorTypePriorities[ b.type ] ) ) {
    198 						bPriority = errorTypePriorities[ b.type ];
    199 					}
    200 					if ( aPriority !== bPriority ) {
    201 						return bPriority - aPriority; // Show errors first.
    202 					}
    203 					return collection._addedOrder[ b.code ] - collection._addedOrder[ a.code ]; // Show newer notifications higher.
    204 				});
    205 			}
    206 
    207 			return notifications;
    208 		},
    209 
    210 		/**
    211 		 * Render notifications area.
    212 		 *
    213 		 * @since 4.9.0
    214 		 * @return {void}
    215 		 */
    216 		render: function() {
    217 			var collection = this,
    218 				notifications, hadOverlayNotification = false, hasOverlayNotification, overlayNotifications = [],
    219 				previousNotificationsByCode = {},
    220 				listElement, focusableElements;
    221 
    222 			// Short-circuit if there are no container to render into.
    223 			if ( ! collection.container || ! collection.container.length ) {
    224 				return;
    225 			}
    226 
    227 			notifications = collection.get( { sort: true } );
    228 			collection.container.toggle( 0 !== notifications.length );
    229 
    230 			// Short-circuit if there are no changes to the notifications.
    231 			if ( collection.container.is( collection.previousContainer ) && _.isEqual( notifications, collection.previousNotifications ) ) {
    232 				return;
    233 			}
    234 
    235 			// Make sure list is part of the container.
    236 			listElement = collection.container.children( 'ul' ).first();
    237 			if ( ! listElement.length ) {
    238 				listElement = $( '<ul></ul>' );
    239 				collection.container.append( listElement );
    240 			}
    241 
    242 			// Remove all notifications prior to re-rendering.
    243 			listElement.find( '> [data-code]' ).remove();
    244 
    245 			_.each( collection.previousNotifications, function( notification ) {
    246 				previousNotificationsByCode[ notification.code ] = notification;
    247 			});
    248 
    249 			// Add all notifications in the sorted order.
    250 			_.each( notifications, function( notification ) {
    251 				var notificationContainer;
    252 				if ( wp.a11y && ( ! previousNotificationsByCode[ notification.code ] || ! _.isEqual( notification.message, previousNotificationsByCode[ notification.code ].message ) ) ) {
    253 					wp.a11y.speak( notification.message, 'assertive' );
    254 				}
    255 				notificationContainer = $( notification.render() );
    256 				notification.container = notificationContainer;
    257 				listElement.append( notificationContainer ); // @todo Consider slideDown() as enhancement.
    258 
    259 				if ( notification.extended( api.OverlayNotification ) ) {
    260 					overlayNotifications.push( notification );
    261 				}
    262 			});
    263 			hasOverlayNotification = Boolean( overlayNotifications.length );
    264 
    265 			if ( collection.previousNotifications ) {
    266 				hadOverlayNotification = Boolean( _.find( collection.previousNotifications, function( notification ) {
    267 					return notification.extended( api.OverlayNotification );
    268 				} ) );
    269 			}
    270 
    271 			if ( hasOverlayNotification !== hadOverlayNotification ) {
    272 				$( document.body ).toggleClass( 'customize-loading', hasOverlayNotification );
    273 				collection.container.toggleClass( 'has-overlay-notifications', hasOverlayNotification );
    274 				if ( hasOverlayNotification ) {
    275 					collection.previousActiveElement = document.activeElement;
    276 					$( document ).on( 'keydown', collection.constrainFocus );
    277 				} else {
    278 					$( document ).off( 'keydown', collection.constrainFocus );
    279 				}
    280 			}
    281 
    282 			if ( hasOverlayNotification ) {
    283 				collection.focusContainer = overlayNotifications[ overlayNotifications.length - 1 ].container;
    284 				collection.focusContainer.prop( 'tabIndex', -1 );
    285 				focusableElements = collection.focusContainer.find( ':focusable' );
    286 				if ( focusableElements.length ) {
    287 					focusableElements.first().focus();
    288 				} else {
    289 					collection.focusContainer.focus();
    290 				}
    291 			} else if ( collection.previousActiveElement ) {
    292 				$( collection.previousActiveElement ).trigger( 'focus' );
    293 				collection.previousActiveElement = null;
    294 			}
    295 
    296 			collection.previousNotifications = notifications;
    297 			collection.previousContainer = collection.container;
    298 			collection.trigger( 'rendered' );
    299 		},
    300 
    301 		/**
    302 		 * Constrain focus on focus container.
    303 		 *
    304 		 * @since 4.9.0
    305 		 *
    306 		 * @param {jQuery.Event} event - Event.
    307 		 * @return {void}
    308 		 */
    309 		constrainFocus: function constrainFocus( event ) {
    310 			var collection = this, focusableElements;
    311 
    312 			// Prevent keys from escaping.
    313 			event.stopPropagation();
    314 
    315 			if ( 9 !== event.which ) { // Tab key.
    316 				return;
    317 			}
    318 
    319 			focusableElements = collection.focusContainer.find( ':focusable' );
    320 			if ( 0 === focusableElements.length ) {
    321 				focusableElements = collection.focusContainer;
    322 			}
    323 
    324 			if ( ! $.contains( collection.focusContainer[0], event.target ) || ! $.contains( collection.focusContainer[0], document.activeElement ) ) {
    325 				event.preventDefault();
    326 				focusableElements.first().focus();
    327 			} else if ( focusableElements.last().is( event.target ) && ! event.shiftKey ) {
    328 				event.preventDefault();
    329 				focusableElements.first().focus();
    330 			} else if ( focusableElements.first().is( event.target ) && event.shiftKey ) {
    331 				event.preventDefault();
    332 				focusableElements.last().focus();
    333 			}
    334 		}
    335 	});
    336 
    337 	api.Setting = api.Value.extend(/** @lends wp.customize.Setting.prototype */{
    338 
    339 		/**
    340 		 * Default params.
    341 		 *
    342 		 * @since 4.9.0
    343 		 * @var {object}
    344 		 */
    345 		defaults: {
    346 			transport: 'refresh',
    347 			dirty: false
    348 		},
    349 
    350 		/**
    351 		 * A Customizer Setting.
    352 		 *
    353 		 * A setting is WordPress data (theme mod, option, menu, etc.) that the user can
    354 		 * draft changes to in the Customizer.
    355 		 *
    356 		 * @see PHP class WP_Customize_Setting.
    357 		 *
    358 		 * @constructs wp.customize.Setting
    359 		 * @augments   wp.customize.Value
    360 		 *
    361 		 * @since 3.4.0
    362 		 *
    363 		 * @param {string}  id                          - The setting ID.
    364 		 * @param {*}       value                       - The initial value of the setting.
    365 		 * @param {Object}  [options={}]                - Options.
    366 		 * @param {string}  [options.transport=refresh] - The transport to use for previewing. Supports 'refresh' and 'postMessage'.
    367 		 * @param {boolean} [options.dirty=false]       - Whether the setting should be considered initially dirty.
    368 		 * @param {Object}  [options.previewer]         - The Previewer instance to sync with. Defaults to wp.customize.previewer.
    369 		 */
    370 		initialize: function( id, value, options ) {
    371 			var setting = this, params;
    372 			params = _.extend(
    373 				{ previewer: api.previewer },
    374 				setting.defaults,
    375 				options || {}
    376 			);
    377 
    378 			api.Value.prototype.initialize.call( setting, value, params );
    379 
    380 			setting.id = id;
    381 			setting._dirty = params.dirty; // The _dirty property is what the Customizer reads from.
    382 			setting.notifications = new api.Notifications();
    383 
    384 			// Whenever the setting's value changes, refresh the preview.
    385 			setting.bind( setting.preview );
    386 		},
    387 
    388 		/**
    389 		 * Refresh the preview, respective of the setting's refresh policy.
    390 		 *
    391 		 * If the preview hasn't sent a keep-alive message and is likely
    392 		 * disconnected by having navigated to a non-allowed URL, then the
    393 		 * refresh transport will be forced when postMessage is the transport.
    394 		 * Note that postMessage does not throw an error when the recipient window
    395 		 * fails to match the origin window, so using try/catch around the
    396 		 * previewer.send() call to then fallback to refresh will not work.
    397 		 *
    398 		 * @since 3.4.0
    399 		 * @access public
    400 		 *
    401 		 * @return {void}
    402 		 */
    403 		preview: function() {
    404 			var setting = this, transport;
    405 			transport = setting.transport;
    406 
    407 			if ( 'postMessage' === transport && ! api.state( 'previewerAlive' ).get() ) {
    408 				transport = 'refresh';
    409 			}
    410 
    411 			if ( 'postMessage' === transport ) {
    412 				setting.previewer.send( 'setting', [ setting.id, setting() ] );
    413 			} else if ( 'refresh' === transport ) {
    414 				setting.previewer.refresh();
    415 			}
    416 		},
    417 
    418 		/**
    419 		 * Find controls associated with this setting.
    420 		 *
    421 		 * @since 4.6.0
    422 		 * @return {wp.customize.Control[]} Controls associated with setting.
    423 		 */
    424 		findControls: function() {
    425 			var setting = this, controls = [];
    426 			api.control.each( function( control ) {
    427 				_.each( control.settings, function( controlSetting ) {
    428 					if ( controlSetting.id === setting.id ) {
    429 						controls.push( control );
    430 					}
    431 				} );
    432 			} );
    433 			return controls;
    434 		}
    435 	});
    436 
    437 	/**
    438 	 * Current change count.
    439 	 *
    440 	 * @alias wp.customize._latestRevision
    441 	 *
    442 	 * @since 4.7.0
    443 	 * @type {number}
    444 	 * @protected
    445 	 */
    446 	api._latestRevision = 0;
    447 
    448 	/**
    449 	 * Last revision that was saved.
    450 	 *
    451 	 * @alias wp.customize._lastSavedRevision
    452 	 *
    453 	 * @since 4.7.0
    454 	 * @type {number}
    455 	 * @protected
    456 	 */
    457 	api._lastSavedRevision = 0;
    458 
    459 	/**
    460 	 * Latest revisions associated with the updated setting.
    461 	 *
    462 	 * @alias wp.customize._latestSettingRevisions
    463 	 *
    464 	 * @since 4.7.0
    465 	 * @type {object}
    466 	 * @protected
    467 	 */
    468 	api._latestSettingRevisions = {};
    469 
    470 	/*
    471 	 * Keep track of the revision associated with each updated setting so that
    472 	 * requestChangesetUpdate knows which dirty settings to include. Also, once
    473 	 * ready is triggered and all initial settings have been added, increment
    474 	 * revision for each newly-created initially-dirty setting so that it will
    475 	 * also be included in changeset update requests.
    476 	 */
    477 	api.bind( 'change', function incrementChangedSettingRevision( setting ) {
    478 		api._latestRevision += 1;
    479 		api._latestSettingRevisions[ setting.id ] = api._latestRevision;
    480 	} );
    481 	api.bind( 'ready', function() {
    482 		api.bind( 'add', function incrementCreatedSettingRevision( setting ) {
    483 			if ( setting._dirty ) {
    484 				api._latestRevision += 1;
    485 				api._latestSettingRevisions[ setting.id ] = api._latestRevision;
    486 			}
    487 		} );
    488 	} );
    489 
    490 	/**
    491 	 * Get the dirty setting values.
    492 	 *
    493 	 * @alias wp.customize.dirtyValues
    494 	 *
    495 	 * @since 4.7.0
    496 	 * @access public
    497 	 *
    498 	 * @param {Object} [options] Options.
    499 	 * @param {boolean} [options.unsaved=false] Whether only values not saved yet into a changeset will be returned (differential changes).
    500 	 * @return {Object} Dirty setting values.
    501 	 */
    502 	api.dirtyValues = function dirtyValues( options ) {
    503 		var values = {};
    504 		api.each( function( setting ) {
    505 			var settingRevision;
    506 
    507 			if ( ! setting._dirty ) {
    508 				return;
    509 			}
    510 
    511 			settingRevision = api._latestSettingRevisions[ setting.id ];
    512 
    513 			// Skip including settings that have already been included in the changeset, if only requesting unsaved.
    514 			if ( api.state( 'changesetStatus' ).get() && ( options && options.unsaved ) && ( _.isUndefined( settingRevision ) || settingRevision <= api._lastSavedRevision ) ) {
    515 				return;
    516 			}
    517 
    518 			values[ setting.id ] = setting.get();
    519 		} );
    520 		return values;
    521 	};
    522 
    523 	/**
    524 	 * Request updates to the changeset.
    525 	 *
    526 	 * @alias wp.customize.requestChangesetUpdate
    527 	 *
    528 	 * @since 4.7.0
    529 	 * @access public
    530 	 *
    531 	 * @param {Object}  [changes] - Mapping of setting IDs to setting params each normally including a value property, or mapping to null.
    532 	 *                             If not provided, then the changes will still be obtained from unsaved dirty settings.
    533 	 * @param {Object}  [args] - Additional options for the save request.
    534 	 * @param {boolean} [args.autosave=false] - Whether changes will be stored in autosave revision if the changeset has been promoted from an auto-draft.
    535 	 * @param {boolean} [args.force=false] - Send request to update even when there are no changes to submit. This can be used to request the latest status of the changeset on the server.
    536 	 * @param {string}  [args.title] - Title to update in the changeset. Optional.
    537 	 * @param {string}  [args.date] - Date to update in the changeset. Optional.
    538 	 * @return {jQuery.Promise} Promise resolving with the response data.
    539 	 */
    540 	api.requestChangesetUpdate = function requestChangesetUpdate( changes, args ) {
    541 		var deferred, request, submittedChanges = {}, data, submittedArgs;
    542 		deferred = new $.Deferred();
    543 
    544 		// Prevent attempting changeset update while request is being made.
    545 		if ( 0 !== api.state( 'processing' ).get() ) {
    546 			deferred.reject( 'already_processing' );
    547 			return deferred.promise();
    548 		}
    549 
    550 		submittedArgs = _.extend( {
    551 			title: null,
    552 			date: null,
    553 			autosave: false,
    554 			force: false
    555 		}, args );
    556 
    557 		if ( changes ) {
    558 			_.extend( submittedChanges, changes );
    559 		}
    560 
    561 		// Ensure all revised settings (changes pending save) are also included, but not if marked for deletion in changes.
    562 		_.each( api.dirtyValues( { unsaved: true } ), function( dirtyValue, settingId ) {
    563 			if ( ! changes || null !== changes[ settingId ] ) {
    564 				submittedChanges[ settingId ] = _.extend(
    565 					{},
    566 					submittedChanges[ settingId ] || {},
    567 					{ value: dirtyValue }
    568 				);
    569 			}
    570 		} );
    571 
    572 		// Allow plugins to attach additional params to the settings.
    573 		api.trigger( 'changeset-save', submittedChanges, submittedArgs );
    574 
    575 		// Short-circuit when there are no pending changes.
    576 		if ( ! submittedArgs.force && _.isEmpty( submittedChanges ) && null === submittedArgs.title && null === submittedArgs.date ) {
    577 			deferred.resolve( {} );
    578 			return deferred.promise();
    579 		}
    580 
    581 		// A status would cause a revision to be made, and for this wp.customize.previewer.save() should be used.
    582 		// Status is also disallowed for revisions regardless.
    583 		if ( submittedArgs.status ) {
    584 			return deferred.reject( { code: 'illegal_status_in_changeset_update' } ).promise();
    585 		}
    586 
    587 		// Dates not beung allowed for revisions are is a technical limitation of post revisions.
    588 		if ( submittedArgs.date && submittedArgs.autosave ) {
    589 			return deferred.reject( { code: 'illegal_autosave_with_date_gmt' } ).promise();
    590 		}
    591 
    592 		// Make sure that publishing a changeset waits for all changeset update requests to complete.
    593 		api.state( 'processing' ).set( api.state( 'processing' ).get() + 1 );
    594 		deferred.always( function() {
    595 			api.state( 'processing' ).set( api.state( 'processing' ).get() - 1 );
    596 		} );
    597 
    598 		// Ensure that if any plugins add data to save requests by extending query() that they get included here.
    599 		data = api.previewer.query( { excludeCustomizedSaved: true } );
    600 		delete data.customized; // Being sent in customize_changeset_data instead.
    601 		_.extend( data, {
    602 			nonce: api.settings.nonce.save,
    603 			customize_theme: api.settings.theme.stylesheet,
    604 			customize_changeset_data: JSON.stringify( submittedChanges )
    605 		} );
    606 		if ( null !== submittedArgs.title ) {
    607 			data.customize_changeset_title = submittedArgs.title;
    608 		}
    609 		if ( null !== submittedArgs.date ) {
    610 			data.customize_changeset_date = submittedArgs.date;
    611 		}
    612 		if ( false !== submittedArgs.autosave ) {
    613 			data.customize_changeset_autosave = 'true';
    614 		}
    615 
    616 		// Allow plugins to modify the params included with the save request.
    617 		api.trigger( 'save-request-params', data );
    618 
    619 		request = wp.ajax.post( 'customize_save', data );
    620 
    621 		request.done( function requestChangesetUpdateDone( data ) {
    622 			var savedChangesetValues = {};
    623 
    624 			// Ensure that all settings updated subsequently will be included in the next changeset update request.
    625 			api._lastSavedRevision = Math.max( api._latestRevision, api._lastSavedRevision );
    626 
    627 			api.state( 'changesetStatus' ).set( data.changeset_status );
    628 
    629 			if ( data.changeset_date ) {
    630 				api.state( 'changesetDate' ).set( data.changeset_date );
    631 			}
    632 
    633 			deferred.resolve( data );
    634 			api.trigger( 'changeset-saved', data );
    635 
    636 			if ( data.setting_validities ) {
    637 				_.each( data.setting_validities, function( validity, settingId ) {
    638 					if ( true === validity && _.isObject( submittedChanges[ settingId ] ) && ! _.isUndefined( submittedChanges[ settingId ].value ) ) {
    639 						savedChangesetValues[ settingId ] = submittedChanges[ settingId ].value;
    640 					}
    641 				} );
    642 			}
    643 
    644 			api.previewer.send( 'changeset-saved', _.extend( {}, data, { saved_changeset_values: savedChangesetValues } ) );
    645 		} );
    646 		request.fail( function requestChangesetUpdateFail( data ) {
    647 			deferred.reject( data );
    648 			api.trigger( 'changeset-error', data );
    649 		} );
    650 		request.always( function( data ) {
    651 			if ( data.setting_validities ) {
    652 				api._handleSettingValidities( {
    653 					settingValidities: data.setting_validities
    654 				} );
    655 			}
    656 		} );
    657 
    658 		return deferred.promise();
    659 	};
    660 
    661 	/**
    662 	 * Watch all changes to Value properties, and bubble changes to parent Values instance
    663 	 *
    664 	 * @alias wp.customize.utils.bubbleChildValueChanges
    665 	 *
    666 	 * @since 4.1.0
    667 	 *
    668 	 * @param {wp.customize.Class} instance
    669 	 * @param {Array}              properties  The names of the Value instances to watch.
    670 	 */
    671 	api.utils.bubbleChildValueChanges = function ( instance, properties ) {
    672 		$.each( properties, function ( i, key ) {
    673 			instance[ key ].bind( function ( to, from ) {
    674 				if ( instance.parent && to !== from ) {
    675 					instance.parent.trigger( 'change', instance );
    676 				}
    677 			} );
    678 		} );
    679 	};
    680 
    681 	/**
    682 	 * Expand a panel, section, or control and focus on the first focusable element.
    683 	 *
    684 	 * @alias wp.customize~focus
    685 	 *
    686 	 * @since 4.1.0
    687 	 *
    688 	 * @param {Object}   [params]
    689 	 * @param {Function} [params.completeCallback]
    690 	 */
    691 	focus = function ( params ) {
    692 		var construct, completeCallback, focus, focusElement;
    693 		construct = this;
    694 		params = params || {};
    695 		focus = function () {
    696 			var focusContainer;
    697 			if ( ( construct.extended( api.Panel ) || construct.extended( api.Section ) ) && construct.expanded && construct.expanded() ) {
    698 				focusContainer = construct.contentContainer;
    699 			} else {
    700 				focusContainer = construct.container;
    701 			}
    702 
    703 			focusElement = focusContainer.find( '.control-focus:first' );
    704 			if ( 0 === focusElement.length ) {
    705 				// Note that we can't use :focusable due to a jQuery UI issue. See: https://github.com/jquery/jquery-ui/pull/1583
    706 				focusElement = focusContainer.find( 'input, select, textarea, button, object, a[href], [tabindex]' ).filter( ':visible' ).first();
    707 			}
    708 			focusElement.focus();
    709 		};
    710 		if ( params.completeCallback ) {
    711 			completeCallback = params.completeCallback;
    712 			params.completeCallback = function () {
    713 				focus();
    714 				completeCallback();
    715 			};
    716 		} else {
    717 			params.completeCallback = focus;
    718 		}
    719 
    720 		api.state( 'paneVisible' ).set( true );
    721 		if ( construct.expand ) {
    722 			construct.expand( params );
    723 		} else {
    724 			params.completeCallback();
    725 		}
    726 	};
    727 
    728 	/**
    729 	 * Stable sort for Panels, Sections, and Controls.
    730 	 *
    731 	 * If a.priority() === b.priority(), then sort by their respective params.instanceNumber.
    732 	 *
    733 	 * @alias wp.customize.utils.prioritySort
    734 	 *
    735 	 * @since 4.1.0
    736 	 *
    737 	 * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} a
    738 	 * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} b
    739 	 * @return {number}
    740 	 */
    741 	api.utils.prioritySort = function ( a, b ) {
    742 		if ( a.priority() === b.priority() && typeof a.params.instanceNumber === 'number' && typeof b.params.instanceNumber === 'number' ) {
    743 			return a.params.instanceNumber - b.params.instanceNumber;
    744 		} else {
    745 			return a.priority() - b.priority();
    746 		}
    747 	};
    748 
    749 	/**
    750 	 * Return whether the supplied Event object is for a keydown event but not the Enter key.
    751 	 *
    752 	 * @alias wp.customize.utils.isKeydownButNotEnterEvent
    753 	 *
    754 	 * @since 4.1.0
    755 	 *
    756 	 * @param {jQuery.Event} event
    757 	 * @return {boolean}
    758 	 */
    759 	api.utils.isKeydownButNotEnterEvent = function ( event ) {
    760 		return ( 'keydown' === event.type && 13 !== event.which );
    761 	};
    762 
    763 	/**
    764 	 * Return whether the two lists of elements are the same and are in the same order.
    765 	 *
    766 	 * @alias wp.customize.utils.areElementListsEqual
    767 	 *
    768 	 * @since 4.1.0
    769 	 *
    770 	 * @param {Array|jQuery} listA
    771 	 * @param {Array|jQuery} listB
    772 	 * @return {boolean}
    773 	 */
    774 	api.utils.areElementListsEqual = function ( listA, listB ) {
    775 		var equal = (
    776 			listA.length === listB.length && // If lists are different lengths, then naturally they are not equal.
    777 			-1 === _.indexOf( _.map(         // Are there any false values in the list returned by map?
    778 				_.zip( listA, listB ),       // Pair up each element between the two lists.
    779 				function ( pair ) {
    780 					return $( pair[0] ).is( pair[1] ); // Compare to see if each pair is equal.
    781 				}
    782 			), false ) // Check for presence of false in map's return value.
    783 		);
    784 		return equal;
    785 	};
    786 
    787 	/**
    788 	 * Highlight the existence of a button.
    789 	 *
    790 	 * This function reminds the user of a button represented by the specified
    791 	 * UI element, after an optional delay. If the user focuses the element
    792 	 * before the delay passes, the reminder is canceled.
    793 	 *
    794 	 * @alias wp.customize.utils.highlightButton
    795 	 *
    796 	 * @since 4.9.0
    797 	 *
    798 	 * @param {jQuery} button - The element to highlight.
    799 	 * @param {Object} [options] - Options.
    800 	 * @param {number} [options.delay=0] - Delay in milliseconds.
    801 	 * @param {jQuery} [options.focusTarget] - A target for user focus that defaults to the highlighted element.
    802 	 *                                         If the user focuses the target before the delay passes, the reminder
    803 	 *                                         is canceled. This option exists to accommodate compound buttons
    804 	 *                                         containing auxiliary UI, such as the Publish button augmented with a
    805 	 *                                         Settings button.
    806 	 * @return {Function} An idempotent function that cancels the reminder.
    807 	 */
    808 	api.utils.highlightButton = function highlightButton( button, options ) {
    809 		var animationClass = 'button-see-me',
    810 			canceled = false,
    811 			params;
    812 
    813 		params = _.extend(
    814 			{
    815 				delay: 0,
    816 				focusTarget: button
    817 			},
    818 			options
    819 		);
    820 
    821 		function cancelReminder() {
    822 			canceled = true;
    823 		}
    824 
    825 		params.focusTarget.on( 'focusin', cancelReminder );
    826 		setTimeout( function() {
    827 			params.focusTarget.off( 'focusin', cancelReminder );
    828 
    829 			if ( ! canceled ) {
    830 				button.addClass( animationClass );
    831 				button.one( 'animationend', function() {
    832 					/*
    833 					 * Remove animation class to avoid situations in Customizer where
    834 					 * DOM nodes are moved (re-inserted) and the animation repeats.
    835 					 */
    836 					button.removeClass( animationClass );
    837 				} );
    838 			}
    839 		}, params.delay );
    840 
    841 		return cancelReminder;
    842 	};
    843 
    844 	/**
    845 	 * Get current timestamp adjusted for server clock time.
    846 	 *
    847 	 * Same functionality as the `current_time( 'mysql', false )` function in PHP.
    848 	 *
    849 	 * @alias wp.customize.utils.getCurrentTimestamp
    850 	 *
    851 	 * @since 4.9.0
    852 	 *
    853 	 * @return {number} Current timestamp.
    854 	 */
    855 	api.utils.getCurrentTimestamp = function getCurrentTimestamp() {
    856 		var currentDate, currentClientTimestamp, timestampDifferential;
    857 		currentClientTimestamp = _.now();
    858 		currentDate = new Date( api.settings.initialServerDate.replace( /-/g, '/' ) );
    859 		timestampDifferential = currentClientTimestamp - api.settings.initialClientTimestamp;
    860 		timestampDifferential += api.settings.initialClientTimestamp - api.settings.initialServerTimestamp;
    861 		currentDate.setTime( currentDate.getTime() + timestampDifferential );
    862 		return currentDate.getTime();
    863 	};
    864 
    865 	/**
    866 	 * Get remaining time of when the date is set.
    867 	 *
    868 	 * @alias wp.customize.utils.getRemainingTime
    869 	 *
    870 	 * @since 4.9.0
    871 	 *
    872 	 * @param {string|number|Date} datetime - Date time or timestamp of the future date.
    873 	 * @return {number} remainingTime - Remaining time in milliseconds.
    874 	 */
    875 	api.utils.getRemainingTime = function getRemainingTime( datetime ) {
    876 		var millisecondsDivider = 1000, remainingTime, timestamp;
    877 		if ( datetime instanceof Date ) {
    878 			timestamp = datetime.getTime();
    879 		} else if ( 'string' === typeof datetime ) {
    880 			timestamp = ( new Date( datetime.replace( /-/g, '/' ) ) ).getTime();
    881 		} else {
    882 			timestamp = datetime;
    883 		}
    884 
    885 		remainingTime = timestamp - api.utils.getCurrentTimestamp();
    886 		remainingTime = Math.ceil( remainingTime / millisecondsDivider );
    887 		return remainingTime;
    888 	};
    889 
    890 	/**
    891 	 * Return browser supported `transitionend` event name.
    892 	 *
    893 	 * @since 4.7.0
    894 	 *
    895 	 * @ignore
    896 	 *
    897 	 * @return {string|null} Normalized `transitionend` event name or null if CSS transitions are not supported.
    898 	 */
    899 	normalizedTransitionendEventName = (function () {
    900 		var el, transitions, prop;
    901 		el = document.createElement( 'div' );
    902 		transitions = {
    903 			'transition'      : 'transitionend',
    904 			'OTransition'     : 'oTransitionEnd',
    905 			'MozTransition'   : 'transitionend',
    906 			'WebkitTransition': 'webkitTransitionEnd'
    907 		};
    908 		prop = _.find( _.keys( transitions ), function( prop ) {
    909 			return ! _.isUndefined( el.style[ prop ] );
    910 		} );
    911 		if ( prop ) {
    912 			return transitions[ prop ];
    913 		} else {
    914 			return null;
    915 		}
    916 	})();
    917 
    918 	Container = api.Class.extend(/** @lends wp.customize~Container.prototype */{
    919 		defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
    920 		defaultExpandedArguments: { duration: 'fast', completeCallback: $.noop },
    921 		containerType: 'container',
    922 		defaults: {
    923 			title: '',
    924 			description: '',
    925 			priority: 100,
    926 			type: 'default',
    927 			content: null,
    928 			active: true,
    929 			instanceNumber: null
    930 		},
    931 
    932 		/**
    933 		 * Base class for Panel and Section.
    934 		 *
    935 		 * @constructs wp.customize~Container
    936 		 * @augments   wp.customize.Class
    937 		 *
    938 		 * @since 4.1.0
    939 		 *
    940 		 * @borrows wp.customize~focus as focus
    941 		 *
    942 		 * @param {string}  id - The ID for the container.
    943 		 * @param {Object}  options - Object containing one property: params.
    944 		 * @param {string}  options.title - Title shown when panel is collapsed and expanded.
    945 		 * @param {string}  [options.description] - Description shown at the top of the panel.
    946 		 * @param {number}  [options.priority=100] - The sort priority for the panel.
    947 		 * @param {string}  [options.templateId] - Template selector for container.
    948 		 * @param {string}  [options.type=default] - The type of the panel. See wp.customize.panelConstructor.
    949 		 * @param {string}  [options.content] - The markup to be used for the panel container. If empty, a JS template is used.
    950 		 * @param {boolean} [options.active=true] - Whether the panel is active or not.
    951 		 * @param {Object}  [options.params] - Deprecated wrapper for the above properties.
    952 		 */
    953 		initialize: function ( id, options ) {
    954 			var container = this;
    955 			container.id = id;
    956 
    957 			if ( ! Container.instanceCounter ) {
    958 				Container.instanceCounter = 0;
    959 			}
    960 			Container.instanceCounter++;
    961 
    962 			$.extend( container, {
    963 				params: _.defaults(
    964 					options.params || options, // Passing the params is deprecated.
    965 					container.defaults
    966 				)
    967 			} );
    968 			if ( ! container.params.instanceNumber ) {
    969 				container.params.instanceNumber = Container.instanceCounter;
    970 			}
    971 			container.notifications = new api.Notifications();
    972 			container.templateSelector = container.params.templateId || 'customize-' + container.containerType + '-' + container.params.type;
    973 			container.container = $( container.params.content );
    974 			if ( 0 === container.container.length ) {
    975 				container.container = $( container.getContainer() );
    976 			}
    977 			container.headContainer = container.container;
    978 			container.contentContainer = container.getContent();
    979 			container.container = container.container.add( container.contentContainer );
    980 
    981 			container.deferred = {
    982 				embedded: new $.Deferred()
    983 			};
    984 			container.priority = new api.Value();
    985 			container.active = new api.Value();
    986 			container.activeArgumentsQueue = [];
    987 			container.expanded = new api.Value();
    988 			container.expandedArgumentsQueue = [];
    989 
    990 			container.active.bind( function ( active ) {
    991 				var args = container.activeArgumentsQueue.shift();
    992 				args = $.extend( {}, container.defaultActiveArguments, args );
    993 				active = ( active && container.isContextuallyActive() );
    994 				container.onChangeActive( active, args );
    995 			});
    996 			container.expanded.bind( function ( expanded ) {
    997 				var args = container.expandedArgumentsQueue.shift();
    998 				args = $.extend( {}, container.defaultExpandedArguments, args );
    999 				container.onChangeExpanded( expanded, args );
   1000 			});
   1001 
   1002 			container.deferred.embedded.done( function () {
   1003 				container.setupNotifications();
   1004 				container.attachEvents();
   1005 			});
   1006 
   1007 			api.utils.bubbleChildValueChanges( container, [ 'priority', 'active' ] );
   1008 
   1009 			container.priority.set( container.params.priority );
   1010 			container.active.set( container.params.active );
   1011 			container.expanded.set( false );
   1012 		},
   1013 
   1014 		/**
   1015 		 * Get the element that will contain the notifications.
   1016 		 *
   1017 		 * @since 4.9.0
   1018 		 * @return {jQuery} Notification container element.
   1019 		 */
   1020 		getNotificationsContainerElement: function() {
   1021 			var container = this;
   1022 			return container.contentContainer.find( '.customize-control-notifications-container:first' );
   1023 		},
   1024 
   1025 		/**
   1026 		 * Set up notifications.
   1027 		 *
   1028 		 * @since 4.9.0
   1029 		 * @return {void}
   1030 		 */
   1031 		setupNotifications: function() {
   1032 			var container = this, renderNotifications;
   1033 			container.notifications.container = container.getNotificationsContainerElement();
   1034 
   1035 			// Render notifications when they change and when the construct is expanded.
   1036 			renderNotifications = function() {
   1037 				if ( container.expanded.get() ) {
   1038 					container.notifications.render();
   1039 				}
   1040 			};
   1041 			container.expanded.bind( renderNotifications );
   1042 			renderNotifications();
   1043 			container.notifications.bind( 'change', _.debounce( renderNotifications ) );
   1044 		},
   1045 
   1046 		/**
   1047 		 * @since 4.1.0
   1048 		 *
   1049 		 * @abstract
   1050 		 */
   1051 		ready: function() {},
   1052 
   1053 		/**
   1054 		 * Get the child models associated with this parent, sorting them by their priority Value.
   1055 		 *
   1056 		 * @since 4.1.0
   1057 		 *
   1058 		 * @param {string} parentType
   1059 		 * @param {string} childType
   1060 		 * @return {Array}
   1061 		 */
   1062 		_children: function ( parentType, childType ) {
   1063 			var parent = this,
   1064 				children = [];
   1065 			api[ childType ].each( function ( child ) {
   1066 				if ( child[ parentType ].get() === parent.id ) {
   1067 					children.push( child );
   1068 				}
   1069 			} );
   1070 			children.sort( api.utils.prioritySort );
   1071 			return children;
   1072 		},
   1073 
   1074 		/**
   1075 		 * To override by subclass, to return whether the container has active children.
   1076 		 *
   1077 		 * @since 4.1.0
   1078 		 *
   1079 		 * @abstract
   1080 		 */
   1081 		isContextuallyActive: function () {
   1082 			throw new Error( 'Container.isContextuallyActive() must be overridden in a subclass.' );
   1083 		},
   1084 
   1085 		/**
   1086 		 * Active state change handler.
   1087 		 *
   1088 		 * Shows the container if it is active, hides it if not.
   1089 		 *
   1090 		 * To override by subclass, update the container's UI to reflect the provided active state.
   1091 		 *
   1092 		 * @since 4.1.0
   1093 		 *
   1094 		 * @param {boolean}  active - The active state to transiution to.
   1095 		 * @param {Object}   [args] - Args.
   1096 		 * @param {Object}   [args.duration] - The duration for the slideUp/slideDown animation.
   1097 		 * @param {boolean}  [args.unchanged] - Whether the state is already known to not be changed, and so short-circuit with calling completeCallback early.
   1098 		 * @param {Function} [args.completeCallback] - Function to call when the slideUp/slideDown has completed.
   1099 		 */
   1100 		onChangeActive: function( active, args ) {
   1101 			var construct = this,
   1102 				headContainer = construct.headContainer,
   1103 				duration, expandedOtherPanel;
   1104 
   1105 			if ( args.unchanged ) {
   1106 				if ( args.completeCallback ) {
   1107 					args.completeCallback();
   1108 				}
   1109 				return;
   1110 			}
   1111 
   1112 			duration = ( 'resolved' === api.previewer.deferred.active.state() ? args.duration : 0 );
   1113 
   1114 			if ( construct.extended( api.Panel ) ) {
   1115 				// If this is a panel is not currently expanded but another panel is expanded, do not animate.
   1116 				api.panel.each(function ( panel ) {
   1117 					if ( panel !== construct && panel.expanded() ) {
   1118 						expandedOtherPanel = panel;
   1119 						duration = 0;
   1120 					}
   1121 				});
   1122 
   1123 				// Collapse any expanded sections inside of this panel first before deactivating.
   1124 				if ( ! active ) {
   1125 					_.each( construct.sections(), function( section ) {
   1126 						section.collapse( { duration: 0 } );
   1127 					} );
   1128 				}
   1129 			}
   1130 
   1131 			if ( ! $.contains( document, headContainer.get( 0 ) ) ) {
   1132 				// If the element is not in the DOM, then jQuery.fn.slideUp() does nothing.
   1133 				// In this case, a hard toggle is required instead.
   1134 				headContainer.toggle( active );
   1135 				if ( args.completeCallback ) {
   1136 					args.completeCallback();
   1137 				}
   1138 			} else if ( active ) {
   1139 				headContainer.slideDown( duration, args.completeCallback );
   1140 			} else {
   1141 				if ( construct.expanded() ) {
   1142 					construct.collapse({
   1143 						duration: duration,
   1144 						completeCallback: function() {
   1145 							headContainer.slideUp( duration, args.completeCallback );
   1146 						}
   1147 					});
   1148 				} else {
   1149 					headContainer.slideUp( duration, args.completeCallback );
   1150 				}
   1151 			}
   1152 		},
   1153 
   1154 		/**
   1155 		 * @since 4.1.0
   1156 		 *
   1157 		 * @param {boolean} active
   1158 		 * @param {Object}  [params]
   1159 		 * @return {boolean} False if state already applied.
   1160 		 */
   1161 		_toggleActive: function ( active, params ) {
   1162 			var self = this;
   1163 			params = params || {};
   1164 			if ( ( active && this.active.get() ) || ( ! active && ! this.active.get() ) ) {
   1165 				params.unchanged = true;
   1166 				self.onChangeActive( self.active.get(), params );
   1167 				return false;
   1168 			} else {
   1169 				params.unchanged = false;
   1170 				this.activeArgumentsQueue.push( params );
   1171 				this.active.set( active );
   1172 				return true;
   1173 			}
   1174 		},
   1175 
   1176 		/**
   1177 		 * @param {Object} [params]
   1178 		 * @return {boolean} False if already active.
   1179 		 */
   1180 		activate: function ( params ) {
   1181 			return this._toggleActive( true, params );
   1182 		},
   1183 
   1184 		/**
   1185 		 * @param {Object} [params]
   1186 		 * @return {boolean} False if already inactive.
   1187 		 */
   1188 		deactivate: function ( params ) {
   1189 			return this._toggleActive( false, params );
   1190 		},
   1191 
   1192 		/**
   1193 		 * To override by subclass, update the container's UI to reflect the provided active state.
   1194 		 * @abstract
   1195 		 */
   1196 		onChangeExpanded: function () {
   1197 			throw new Error( 'Must override with subclass.' );
   1198 		},
   1199 
   1200 		/**
   1201 		 * Handle the toggle logic for expand/collapse.
   1202 		 *
   1203 		 * @param {boolean}  expanded - The new state to apply.
   1204 		 * @param {Object}   [params] - Object containing options for expand/collapse.
   1205 		 * @param {Function} [params.completeCallback] - Function to call when expansion/collapse is complete.
   1206 		 * @return {boolean} False if state already applied or active state is false.
   1207 		 */
   1208 		_toggleExpanded: function( expanded, params ) {
   1209 			var instance = this, previousCompleteCallback;
   1210 			params = params || {};
   1211 			previousCompleteCallback = params.completeCallback;
   1212 
   1213 			// Short-circuit expand() if the instance is not active.
   1214 			if ( expanded && ! instance.active() ) {
   1215 				return false;
   1216 			}
   1217 
   1218 			api.state( 'paneVisible' ).set( true );
   1219 			params.completeCallback = function() {
   1220 				if ( previousCompleteCallback ) {
   1221 					previousCompleteCallback.apply( instance, arguments );
   1222 				}
   1223 				if ( expanded ) {
   1224 					instance.container.trigger( 'expanded' );
   1225 				} else {
   1226 					instance.container.trigger( 'collapsed' );
   1227 				}
   1228 			};
   1229 			if ( ( expanded && instance.expanded.get() ) || ( ! expanded && ! instance.expanded.get() ) ) {
   1230 				params.unchanged = true;
   1231 				instance.onChangeExpanded( instance.expanded.get(), params );
   1232 				return false;
   1233 			} else {
   1234 				params.unchanged = false;
   1235 				instance.expandedArgumentsQueue.push( params );
   1236 				instance.expanded.set( expanded );
   1237 				return true;
   1238 			}
   1239 		},
   1240 
   1241 		/**
   1242 		 * @param {Object} [params]
   1243 		 * @return {boolean} False if already expanded or if inactive.
   1244 		 */
   1245 		expand: function ( params ) {
   1246 			return this._toggleExpanded( true, params );
   1247 		},
   1248 
   1249 		/**
   1250 		 * @param {Object} [params]
   1251 		 * @return {boolean} False if already collapsed.
   1252 		 */
   1253 		collapse: function ( params ) {
   1254 			return this._toggleExpanded( false, params );
   1255 		},
   1256 
   1257 		/**
   1258 		 * Animate container state change if transitions are supported by the browser.
   1259 		 *
   1260 		 * @since 4.7.0
   1261 		 * @private
   1262 		 *
   1263 		 * @param {function} completeCallback Function to be called after transition is completed.
   1264 		 * @return {void}
   1265 		 */
   1266 		_animateChangeExpanded: function( completeCallback ) {
   1267 			// Return if CSS transitions are not supported.
   1268 			if ( ! normalizedTransitionendEventName ) {
   1269 				if ( completeCallback ) {
   1270 					completeCallback();
   1271 				}
   1272 				return;
   1273 			}
   1274 
   1275 			var construct = this,
   1276 				content = construct.contentContainer,
   1277 				overlay = content.closest( '.wp-full-overlay' ),
   1278 				elements, transitionEndCallback, transitionParentPane;
   1279 
   1280 			// Determine set of elements that are affected by the animation.
   1281 			elements = overlay.add( content );
   1282 
   1283 			if ( ! construct.panel || '' === construct.panel() ) {
   1284 				transitionParentPane = true;
   1285 			} else if ( api.panel( construct.panel() ).contentContainer.hasClass( 'skip-transition' ) ) {
   1286 				transitionParentPane = true;
   1287 			} else {
   1288 				transitionParentPane = false;
   1289 			}
   1290 			if ( transitionParentPane ) {
   1291 				elements = elements.add( '#customize-info, .customize-pane-parent' );
   1292 			}
   1293 
   1294 			// Handle `transitionEnd` event.
   1295 			transitionEndCallback = function( e ) {
   1296 				if ( 2 !== e.eventPhase || ! $( e.target ).is( content ) ) {
   1297 					return;
   1298 				}
   1299 				content.off( normalizedTransitionendEventName, transitionEndCallback );
   1300 				elements.removeClass( 'busy' );
   1301 				if ( completeCallback ) {
   1302 					completeCallback();
   1303 				}
   1304 			};
   1305 			content.on( normalizedTransitionendEventName, transitionEndCallback );
   1306 			elements.addClass( 'busy' );
   1307 
   1308 			// Prevent screen flicker when pane has been scrolled before expanding.
   1309 			_.defer( function() {
   1310 				var container = content.closest( '.wp-full-overlay-sidebar-content' ),
   1311 					currentScrollTop = container.scrollTop(),
   1312 					previousScrollTop = content.data( 'previous-scrollTop' ) || 0,
   1313 					expanded = construct.expanded();
   1314 
   1315 				if ( expanded && 0 < currentScrollTop ) {
   1316 					content.css( 'top', currentScrollTop + 'px' );
   1317 					content.data( 'previous-scrollTop', currentScrollTop );
   1318 				} else if ( ! expanded && 0 < currentScrollTop + previousScrollTop ) {
   1319 					content.css( 'top', previousScrollTop - currentScrollTop + 'px' );
   1320 					container.scrollTop( previousScrollTop );
   1321 				}
   1322 			} );
   1323 		},
   1324 
   1325 		/*
   1326 		 * is documented using @borrows in the constructor.
   1327 		 */
   1328 		focus: focus,
   1329 
   1330 		/**
   1331 		 * Return the container html, generated from its JS template, if it exists.
   1332 		 *
   1333 		 * @since 4.3.0
   1334 		 */
   1335 		getContainer: function () {
   1336 			var template,
   1337 				container = this;
   1338 
   1339 			if ( 0 !== $( '#tmpl-' + container.templateSelector ).length ) {
   1340 				template = wp.template( container.templateSelector );
   1341 			} else {
   1342 				template = wp.template( 'customize-' + container.containerType + '-default' );
   1343 			}
   1344 			if ( template && container.container ) {
   1345 				return template( _.extend(
   1346 					{ id: container.id },
   1347 					container.params
   1348 				) ).toString().trim();
   1349 			}
   1350 
   1351 			return '<li></li>';
   1352 		},
   1353 
   1354 		/**
   1355 		 * Find content element which is displayed when the section is expanded.
   1356 		 *
   1357 		 * After a construct is initialized, the return value will be available via the `contentContainer` property.
   1358 		 * By default the element will be related it to the parent container with `aria-owns` and detached.
   1359 		 * Custom panels and sections (such as the `NewMenuSection`) that do not have a sliding pane should
   1360 		 * just return the content element without needing to add the `aria-owns` element or detach it from
   1361 		 * the container. Such non-sliding pane custom sections also need to override the `onChangeExpanded`
   1362 		 * method to handle animating the panel/section into and out of view.
   1363 		 *
   1364 		 * @since 4.7.0
   1365 		 * @access public
   1366 		 *
   1367 		 * @return {jQuery} Detached content element.
   1368 		 */
   1369 		getContent: function() {
   1370 			var construct = this,
   1371 				container = construct.container,
   1372 				content = container.find( '.accordion-section-content, .control-panel-content' ).first(),
   1373 				contentId = 'sub-' + container.attr( 'id' ),
   1374 				ownedElements = contentId,
   1375 				alreadyOwnedElements = container.attr( 'aria-owns' );
   1376 
   1377 			if ( alreadyOwnedElements ) {
   1378 				ownedElements = ownedElements + ' ' + alreadyOwnedElements;
   1379 			}
   1380 			container.attr( 'aria-owns', ownedElements );
   1381 
   1382 			return content.detach().attr( {
   1383 				'id': contentId,
   1384 				'class': 'customize-pane-child ' + content.attr( 'class' ) + ' ' + container.attr( 'class' )
   1385 			} );
   1386 		}
   1387 	});
   1388 
   1389 	api.Section = Container.extend(/** @lends wp.customize.Section.prototype */{
   1390 		containerType: 'section',
   1391 		containerParent: '#customize-theme-controls',
   1392 		containerPaneParent: '.customize-pane-parent',
   1393 		defaults: {
   1394 			title: '',
   1395 			description: '',
   1396 			priority: 100,
   1397 			type: 'default',
   1398 			content: null,
   1399 			active: true,
   1400 			instanceNumber: null,
   1401 			panel: null,
   1402 			customizeAction: ''
   1403 		},
   1404 
   1405 		/**
   1406 		 * @constructs wp.customize.Section
   1407 		 * @augments   wp.customize~Container
   1408 		 *
   1409 		 * @since 4.1.0
   1410 		 *
   1411 		 * @param {string}  id - The ID for the section.
   1412 		 * @param {Object}  options - Options.
   1413 		 * @param {string}  options.title - Title shown when section is collapsed and expanded.
   1414 		 * @param {string}  [options.description] - Description shown at the top of the section.
   1415 		 * @param {number}  [options.priority=100] - The sort priority for the section.
   1416 		 * @param {string}  [options.type=default] - The type of the section. See wp.customize.sectionConstructor.
   1417 		 * @param {string}  [options.content] - The markup to be used for the section container. If empty, a JS template is used.
   1418 		 * @param {boolean} [options.active=true] - Whether the section is active or not.
   1419 		 * @param {string}  options.panel - The ID for the panel this section is associated with.
   1420 		 * @param {string}  [options.customizeAction] - Additional context information shown before the section title when expanded.
   1421 		 * @param {Object}  [options.params] - Deprecated wrapper for the above properties.
   1422 		 */
   1423 		initialize: function ( id, options ) {
   1424 			var section = this, params;
   1425 			params = options.params || options;
   1426 
   1427 			// Look up the type if one was not supplied.
   1428 			if ( ! params.type ) {
   1429 				_.find( api.sectionConstructor, function( Constructor, type ) {
   1430 					if ( Constructor === section.constructor ) {
   1431 						params.type = type;
   1432 						return true;
   1433 					}
   1434 					return false;
   1435 				} );
   1436 			}
   1437 
   1438 			Container.prototype.initialize.call( section, id, params );
   1439 
   1440 			section.id = id;
   1441 			section.panel = new api.Value();
   1442 			section.panel.bind( function ( id ) {
   1443 				$( section.headContainer ).toggleClass( 'control-subsection', !! id );
   1444 			});
   1445 			section.panel.set( section.params.panel || '' );
   1446 			api.utils.bubbleChildValueChanges( section, [ 'panel' ] );
   1447 
   1448 			section.embed();
   1449 			section.deferred.embedded.done( function () {
   1450 				section.ready();
   1451 			});
   1452 		},
   1453 
   1454 		/**
   1455 		 * Embed the container in the DOM when any parent panel is ready.
   1456 		 *
   1457 		 * @since 4.1.0
   1458 		 */
   1459 		embed: function () {
   1460 			var inject,
   1461 				section = this;
   1462 
   1463 			section.containerParent = api.ensure( section.containerParent );
   1464 
   1465 			// Watch for changes to the panel state.
   1466 			inject = function ( panelId ) {
   1467 				var parentContainer;
   1468 				if ( panelId ) {
   1469 					// The panel has been supplied, so wait until the panel object is registered.
   1470 					api.panel( panelId, function ( panel ) {
   1471 						// The panel has been registered, wait for it to become ready/initialized.
   1472 						panel.deferred.embedded.done( function () {
   1473 							parentContainer = panel.contentContainer;
   1474 							if ( ! section.headContainer.parent().is( parentContainer ) ) {
   1475 								parentContainer.append( section.headContainer );
   1476 							}
   1477 							if ( ! section.contentContainer.parent().is( section.headContainer ) ) {
   1478 								section.containerParent.append( section.contentContainer );
   1479 							}
   1480 							section.deferred.embedded.resolve();
   1481 						});
   1482 					} );
   1483 				} else {
   1484 					// There is no panel, so embed the section in the root of the customizer.
   1485 					parentContainer = api.ensure( section.containerPaneParent );
   1486 					if ( ! section.headContainer.parent().is( parentContainer ) ) {
   1487 						parentContainer.append( section.headContainer );
   1488 					}
   1489 					if ( ! section.contentContainer.parent().is( section.headContainer ) ) {
   1490 						section.containerParent.append( section.contentContainer );
   1491 					}
   1492 					section.deferred.embedded.resolve();
   1493 				}
   1494 			};
   1495 			section.panel.bind( inject );
   1496 			inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one.
   1497 		},
   1498 
   1499 		/**
   1500 		 * Add behaviors for the accordion section.
   1501 		 *
   1502 		 * @since 4.1.0
   1503 		 */
   1504 		attachEvents: function () {
   1505 			var meta, content, section = this;
   1506 
   1507 			if ( section.container.hasClass( 'cannot-expand' ) ) {
   1508 				return;
   1509 			}
   1510 
   1511 			// Expand/Collapse accordion sections on click.
   1512 			section.container.find( '.accordion-section-title, .customize-section-back' ).on( 'click keydown', function( event ) {
   1513 				if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
   1514 					return;
   1515 				}
   1516 				event.preventDefault(); // Keep this AFTER the key filter above.
   1517 
   1518 				if ( section.expanded() ) {
   1519 					section.collapse();
   1520 				} else {
   1521 					section.expand();
   1522 				}
   1523 			});
   1524 
   1525 			// This is very similar to what is found for api.Panel.attachEvents().
   1526 			section.container.find( '.customize-section-title .customize-help-toggle' ).on( 'click', function() {
   1527 
   1528 				meta = section.container.find( '.section-meta' );
   1529 				if ( meta.hasClass( 'cannot-expand' ) ) {
   1530 					return;
   1531 				}
   1532 				content = meta.find( '.customize-section-description:first' );
   1533 				content.toggleClass( 'open' );
   1534 				content.slideToggle( section.defaultExpandedArguments.duration, function() {
   1535 					content.trigger( 'toggled' );
   1536 				} );
   1537 				$( this ).attr( 'aria-expanded', function( i, attr ) {
   1538 					return 'true' === attr ? 'false' : 'true';
   1539 				});
   1540 			});
   1541 		},
   1542 
   1543 		/**
   1544 		 * Return whether this section has any active controls.
   1545 		 *
   1546 		 * @since 4.1.0
   1547 		 *
   1548 		 * @return {boolean}
   1549 		 */
   1550 		isContextuallyActive: function () {
   1551 			var section = this,
   1552 				controls = section.controls(),
   1553 				activeCount = 0;
   1554 			_( controls ).each( function ( control ) {
   1555 				if ( control.active() ) {
   1556 					activeCount += 1;
   1557 				}
   1558 			} );
   1559 			return ( activeCount !== 0 );
   1560 		},
   1561 
   1562 		/**
   1563 		 * Get the controls that are associated with this section, sorted by their priority Value.
   1564 		 *
   1565 		 * @since 4.1.0
   1566 		 *
   1567 		 * @return {Array}
   1568 		 */
   1569 		controls: function () {
   1570 			return this._children( 'section', 'control' );
   1571 		},
   1572 
   1573 		/**
   1574 		 * Update UI to reflect expanded state.
   1575 		 *
   1576 		 * @since 4.1.0
   1577 		 *
   1578 		 * @param {boolean} expanded
   1579 		 * @param {Object}  args
   1580 		 */
   1581 		onChangeExpanded: function ( expanded, args ) {
   1582 			var section = this,
   1583 				container = section.headContainer.closest( '.wp-full-overlay-sidebar-content' ),
   1584 				content = section.contentContainer,
   1585 				overlay = section.headContainer.closest( '.wp-full-overlay' ),
   1586 				backBtn = content.find( '.customize-section-back' ),
   1587 				sectionTitle = section.headContainer.find( '.accordion-section-title' ).first(),
   1588 				expand, panel;
   1589 
   1590 			if ( expanded && ! content.hasClass( 'open' ) ) {
   1591 
   1592 				if ( args.unchanged ) {
   1593 					expand = args.completeCallback;
   1594 				} else {
   1595 					expand = function() {
   1596 						section._animateChangeExpanded( function() {
   1597 							sectionTitle.attr( 'tabindex', '-1' );
   1598 							backBtn.attr( 'tabindex', '0' );
   1599 
   1600 							backBtn.trigger( 'focus' );
   1601 							content.css( 'top', '' );
   1602 							container.scrollTop( 0 );
   1603 
   1604 							if ( args.completeCallback ) {
   1605 								args.completeCallback();
   1606 							}
   1607 						} );
   1608 
   1609 						content.addClass( 'open' );
   1610 						overlay.addClass( 'section-open' );
   1611 						api.state( 'expandedSection' ).set( section );
   1612 					}.bind( this );
   1613 				}
   1614 
   1615 				if ( ! args.allowMultiple ) {
   1616 					api.section.each( function ( otherSection ) {
   1617 						if ( otherSection !== section ) {
   1618 							otherSection.collapse( { duration: args.duration } );
   1619 						}
   1620 					});
   1621 				}
   1622 
   1623 				if ( section.panel() ) {
   1624 					api.panel( section.panel() ).expand({
   1625 						duration: args.duration,
   1626 						completeCallback: expand
   1627 					});
   1628 				} else {
   1629 					if ( ! args.allowMultiple ) {
   1630 						api.panel.each( function( panel ) {
   1631 							panel.collapse();
   1632 						});
   1633 					}
   1634 					expand();
   1635 				}
   1636 
   1637 			} else if ( ! expanded && content.hasClass( 'open' ) ) {
   1638 				if ( section.panel() ) {
   1639 					panel = api.panel( section.panel() );
   1640 					if ( panel.contentContainer.hasClass( 'skip-transition' ) ) {
   1641 						panel.collapse();
   1642 					}
   1643 				}
   1644 				section._animateChangeExpanded( function() {
   1645 					backBtn.attr( 'tabindex', '-1' );
   1646 					sectionTitle.attr( 'tabindex', '0' );
   1647 
   1648 					sectionTitle.trigger( 'focus' );
   1649 					content.css( 'top', '' );
   1650 
   1651 					if ( args.completeCallback ) {
   1652 						args.completeCallback();
   1653 					}
   1654 				} );
   1655 
   1656 				content.removeClass( 'open' );
   1657 				overlay.removeClass( 'section-open' );
   1658 				if ( section === api.state( 'expandedSection' ).get() ) {
   1659 					api.state( 'expandedSection' ).set( false );
   1660 				}
   1661 
   1662 			} else {
   1663 				if ( args.completeCallback ) {
   1664 					args.completeCallback();
   1665 				}
   1666 			}
   1667 		}
   1668 	});
   1669 
   1670 	api.ThemesSection = api.Section.extend(/** @lends wp.customize.ThemesSection.prototype */{
   1671 		currentTheme: '',
   1672 		overlay: '',
   1673 		template: '',
   1674 		screenshotQueue: null,
   1675 		$window: null,
   1676 		$body: null,
   1677 		loaded: 0,
   1678 		loading: false,
   1679 		fullyLoaded: false,
   1680 		term: '',
   1681 		tags: '',
   1682 		nextTerm: '',
   1683 		nextTags: '',
   1684 		filtersHeight: 0,
   1685 		headerContainer: null,
   1686 		updateCountDebounced: null,
   1687 
   1688 		/**
   1689 		 * wp.customize.ThemesSection
   1690 		 *
   1691 		 * Custom section for themes that loads themes by category, and also
   1692 		 * handles the theme-details view rendering and navigation.
   1693 		 *
   1694 		 * @constructs wp.customize.ThemesSection
   1695 		 * @augments   wp.customize.Section
   1696 		 *
   1697 		 * @since 4.9.0
   1698 		 *
   1699 		 * @param {string} id - ID.
   1700 		 * @param {Object} options - Options.
   1701 		 * @return {void}
   1702 		 */
   1703 		initialize: function( id, options ) {
   1704 			var section = this;
   1705 			section.headerContainer = $();
   1706 			section.$window = $( window );
   1707 			section.$body = $( document.body );
   1708 			api.Section.prototype.initialize.call( section, id, options );
   1709 			section.updateCountDebounced = _.debounce( section.updateCount, 500 );
   1710 		},
   1711 
   1712 		/**
   1713 		 * Embed the section in the DOM when the themes panel is ready.
   1714 		 *
   1715 		 * Insert the section before the themes container. Assume that a themes section is within a panel, but not necessarily the themes panel.
   1716 		 *
   1717 		 * @since 4.9.0
   1718 		 */
   1719 		embed: function() {
   1720 			var inject,
   1721 				section = this;
   1722 
   1723 			// Watch for changes to the panel state.
   1724 			inject = function( panelId ) {
   1725 				var parentContainer;
   1726 				api.panel( panelId, function( panel ) {
   1727 
   1728 					// The panel has been registered, wait for it to become ready/initialized.
   1729 					panel.deferred.embedded.done( function() {
   1730 						parentContainer = panel.contentContainer;
   1731 						if ( ! section.headContainer.parent().is( parentContainer ) ) {
   1732 							parentContainer.find( '.customize-themes-full-container-container' ).before( section.headContainer );
   1733 						}
   1734 						if ( ! section.contentContainer.parent().is( section.headContainer ) ) {
   1735 							section.containerParent.append( section.contentContainer );
   1736 						}
   1737 						section.deferred.embedded.resolve();
   1738 					});
   1739 				} );
   1740 			};
   1741 			section.panel.bind( inject );
   1742 			inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one.
   1743 		},
   1744 
   1745 		/**
   1746 		 * Set up.
   1747 		 *
   1748 		 * @since 4.2.0
   1749 		 *
   1750 		 * @return {void}
   1751 		 */
   1752 		ready: function() {
   1753 			var section = this;
   1754 			section.overlay = section.container.find( '.theme-overlay' );
   1755 			section.template = wp.template( 'customize-themes-details-view' );
   1756 
   1757 			// Bind global keyboard events.
   1758 			section.container.on( 'keydown', function( event ) {
   1759 				if ( ! section.overlay.find( '.theme-wrap' ).is( ':visible' ) ) {
   1760 					return;
   1761 				}
   1762 
   1763 				// Pressing the right arrow key fires a theme:next event.
   1764 				if ( 39 === event.keyCode ) {
   1765 					section.nextTheme();
   1766 				}
   1767 
   1768 				// Pressing the left arrow key fires a theme:previous event.
   1769 				if ( 37 === event.keyCode ) {
   1770 					section.previousTheme();
   1771 				}
   1772 
   1773 				// Pressing the escape key fires a theme:collapse event.
   1774 				if ( 27 === event.keyCode ) {
   1775 					if ( section.$body.hasClass( 'modal-open' ) ) {
   1776 
   1777 						// Escape from the details modal.
   1778 						section.closeDetails();
   1779 					} else {
   1780 
   1781 						// Escape from the inifinite scroll list.
   1782 						section.headerContainer.find( '.customize-themes-section-title' ).focus();
   1783 					}
   1784 					event.stopPropagation(); // Prevent section from being collapsed.
   1785 				}
   1786 			});
   1787 
   1788 			section.renderScreenshots = _.throttle( section.renderScreenshots, 100 );
   1789 
   1790 			_.bindAll( section, 'renderScreenshots', 'loadMore', 'checkTerm', 'filtersChecked' );
   1791 		},
   1792 
   1793 		/**
   1794 		 * Override Section.isContextuallyActive method.
   1795 		 *
   1796 		 * Ignore the active states' of the contained theme controls, and just
   1797 		 * use the section's own active state instead. This prevents empty search
   1798 		 * results for theme sections from causing the section to become inactive.
   1799 		 *
   1800 		 * @since 4.2.0
   1801 		 *
   1802 		 * @return {boolean}
   1803 		 */
   1804 		isContextuallyActive: function () {
   1805 			return this.active();
   1806 		},
   1807 
   1808 		/**
   1809 		 * Attach events.
   1810 		 *
   1811 		 * @since 4.2.0
   1812 		 *
   1813 		 * @return {void}
   1814 		 */
   1815 		attachEvents: function () {
   1816 			var section = this, debounced;
   1817 
   1818 			// Expand/Collapse accordion sections on click.
   1819 			section.container.find( '.customize-section-back' ).on( 'click keydown', function( event ) {
   1820 				if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
   1821 					return;
   1822 				}
   1823 				event.preventDefault(); // Keep this AFTER the key filter above.
   1824 				section.collapse();
   1825 			});
   1826 
   1827 			section.headerContainer = $( '#accordion-section-' + section.id );
   1828 
   1829 			// Expand section/panel. Only collapse when opening another section.
   1830 			section.headerContainer.on( 'click', '.customize-themes-section-title', function() {
   1831 
   1832 				// Toggle accordion filters under section headers.
   1833 				if ( section.headerContainer.find( '.filter-details' ).length ) {
   1834 					section.headerContainer.find( '.customize-themes-section-title' )
   1835 						.toggleClass( 'details-open' )
   1836 						.attr( 'aria-expanded', function( i, attr ) {
   1837 							return 'true' === attr ? 'false' : 'true';
   1838 						});
   1839 					section.headerContainer.find( '.filter-details' ).slideToggle( 180 );
   1840 				}
   1841 
   1842 				// Open the section.
   1843 				if ( ! section.expanded() ) {
   1844 					section.expand();
   1845 				}
   1846 			});
   1847 
   1848 			// Preview installed themes.
   1849 			section.container.on( 'click', '.theme-actions .preview-theme', function() {
   1850 				api.panel( 'themes' ).loadThemePreview( $( this ).data( 'slug' ) );
   1851 			});
   1852 
   1853 			// Theme navigation in details view.
   1854 			section.container.on( 'click', '.left', function() {
   1855 				section.previousTheme();
   1856 			});
   1857 
   1858 			section.container.on( 'click', '.right', function() {
   1859 				section.nextTheme();
   1860 			});
   1861 
   1862 			section.container.on( 'click', '.theme-backdrop, .close', function() {
   1863 				section.closeDetails();
   1864 			});
   1865 
   1866 			if ( 'local' === section.params.filter_type ) {
   1867 
   1868 				// Filter-search all theme objects loaded in the section.
   1869 				section.container.on( 'input', '.wp-filter-search-themes', function( event ) {
   1870 					section.filterSearch( event.currentTarget.value );
   1871 				});
   1872 
   1873 			} else if ( 'remote' === section.params.filter_type ) {
   1874 
   1875 				// Event listeners for remote queries with user-entered terms.
   1876 				// Search terms.
   1877 				debounced = _.debounce( section.checkTerm, 500 ); // Wait until there is no input for 500 milliseconds to initiate a search.
   1878 				section.contentContainer.on( 'input', '.wp-filter-search', function() {
   1879 					if ( ! api.panel( 'themes' ).expanded() ) {
   1880 						return;
   1881 					}
   1882 					debounced( section );
   1883 					if ( ! section.expanded() ) {
   1884 						section.expand();
   1885 					}
   1886 				});
   1887 
   1888 				// Feature filters.
   1889 				section.contentContainer.on( 'click', '.filter-group input', function() {
   1890 					section.filtersChecked();
   1891 					section.checkTerm( section );
   1892 				});
   1893 			}
   1894 
   1895 			// Toggle feature filters.
   1896 			section.contentContainer.on( 'click', '.feature-filter-toggle', function( e ) {
   1897 				var $themeContainer = $( '.customize-themes-full-container' ),
   1898 					$filterToggle = $( e.currentTarget );
   1899 				section.filtersHeight = $filterToggle.parent().next( '.filter-drawer' ).height();
   1900 
   1901 				if ( 0 < $themeContainer.scrollTop() ) {
   1902 					$themeContainer.animate( { scrollTop: 0 }, 400 );
   1903 
   1904 					if ( $filterToggle.hasClass( 'open' ) ) {
   1905 						return;
   1906 					}
   1907 				}
   1908 
   1909 				$filterToggle
   1910 					.toggleClass( 'open' )
   1911 					.attr( 'aria-expanded', function( i, attr ) {
   1912 						return 'true' === attr ? 'false' : 'true';
   1913 					})
   1914 					.parent().next( '.filter-drawer' ).slideToggle( 180, 'linear' );
   1915 
   1916 				if ( $filterToggle.hasClass( 'open' ) ) {
   1917 					var marginOffset = 1018 < window.innerWidth ? 50 : 76;
   1918 
   1919 					section.contentContainer.find( '.themes' ).css( 'margin-top', section.filtersHeight + marginOffset );
   1920 				} else {
   1921 					section.contentContainer.find( '.themes' ).css( 'margin-top', 0 );
   1922 				}
   1923 			});
   1924 
   1925 			// Setup section cross-linking.
   1926 			section.contentContainer.on( 'click', '.no-themes-local .search-dotorg-themes', function() {
   1927 				api.section( 'wporg_themes' ).focus();
   1928 			});
   1929 
   1930 			function updateSelectedState() {
   1931 				var el = section.headerContainer.find( '.customize-themes-section-title' );
   1932 				el.toggleClass( 'selected', section.expanded() );
   1933 				el.attr( 'aria-expanded', section.expanded() ? 'true' : 'false' );
   1934 				if ( ! section.expanded() ) {
   1935 					el.removeClass( 'details-open' );
   1936 				}
   1937 			}
   1938 			section.expanded.bind( updateSelectedState );
   1939 			updateSelectedState();
   1940 
   1941 			// Move section controls to the themes area.
   1942 			api.bind( 'ready', function () {
   1943 				section.contentContainer = section.container.find( '.customize-themes-section' );
   1944 				section.contentContainer.appendTo( $( '.customize-themes-full-container' ) );
   1945 				section.container.add( section.headerContainer );
   1946 			});
   1947 		},
   1948 
   1949 		/**
   1950 		 * Update UI to reflect expanded state
   1951 		 *
   1952 		 * @since 4.2.0
   1953 		 *
   1954 		 * @param {boolean}  expanded
   1955 		 * @param {Object}   args
   1956 		 * @param {boolean}  args.unchanged
   1957 		 * @param {Function} args.completeCallback
   1958 		 * @return {void}
   1959 		 */
   1960 		onChangeExpanded: function ( expanded, args ) {
   1961 
   1962 			// Note: there is a second argument 'args' passed.
   1963 			var section = this,
   1964 				container = section.contentContainer.closest( '.customize-themes-full-container' );
   1965 
   1966 			// Immediately call the complete callback if there were no changes.
   1967 			if ( args.unchanged ) {
   1968 				if ( args.completeCallback ) {
   1969 					args.completeCallback();
   1970 				}
   1971 				return;
   1972 			}
   1973 
   1974 			function expand() {
   1975 
   1976 				// Try to load controls if none are loaded yet.
   1977 				if ( 0 === section.loaded ) {
   1978 					section.loadThemes();
   1979 				}
   1980 
   1981 				// Collapse any sibling sections/panels.
   1982 				api.section.each( function ( otherSection ) {
   1983 					var searchTerm;
   1984 
   1985 					if ( otherSection !== section ) {
   1986 
   1987 						// Try to sync the current search term to the new section.
   1988 						if ( 'themes' === otherSection.params.type ) {
   1989 							searchTerm = otherSection.contentContainer.find( '.wp-filter-search' ).val();
   1990 							section.contentContainer.find( '.wp-filter-search' ).val( searchTerm );
   1991 
   1992 							// Directly initialize an empty remote search to avoid a race condition.
   1993 							if ( '' === searchTerm && '' !== section.term && 'local' !== section.params.filter_type ) {
   1994 								section.term = '';
   1995 								section.initializeNewQuery( section.term, section.tags );
   1996 							} else {
   1997 								if ( 'remote' === section.params.filter_type ) {
   1998 									section.checkTerm( section );
   1999 								} else if ( 'local' === section.params.filter_type ) {
   2000 									section.filterSearch( searchTerm );
   2001 								}
   2002 							}
   2003 							otherSection.collapse( { duration: args.duration } );
   2004 						}
   2005 					}
   2006 				});
   2007 
   2008 				section.contentContainer.addClass( 'current-section' );
   2009 				container.scrollTop();
   2010 
   2011 				container.on( 'scroll', _.throttle( section.renderScreenshots, 300 ) );
   2012 				container.on( 'scroll', _.throttle( section.loadMore, 300 ) );
   2013 
   2014 				if ( args.completeCallback ) {
   2015 					args.completeCallback();
   2016 				}
   2017 				section.updateCount(); // Show this section's count.
   2018 			}
   2019 
   2020 			if ( expanded ) {
   2021 				if ( section.panel() && api.panel.has( section.panel() ) ) {
   2022 					api.panel( section.panel() ).expand({
   2023 						duration: args.duration,
   2024 						completeCallback: expand
   2025 					});
   2026 				} else {
   2027 					expand();
   2028 				}
   2029 			} else {
   2030 				section.contentContainer.removeClass( 'current-section' );
   2031 
   2032 				// Always hide, even if they don't exist or are already hidden.
   2033 				section.headerContainer.find( '.filter-details' ).slideUp( 180 );
   2034 
   2035 				container.off( 'scroll' );
   2036 
   2037 				if ( args.completeCallback ) {
   2038 					args.completeCallback();
   2039 				}
   2040 			}
   2041 		},
   2042 
   2043 		/**
   2044 		 * Return the section's content element without detaching from the parent.
   2045 		 *
   2046 		 * @since 4.9.0
   2047 		 *
   2048 		 * @return {jQuery}
   2049 		 */
   2050 		getContent: function() {
   2051 			return this.container.find( '.control-section-content' );
   2052 		},
   2053 
   2054 		/**
   2055 		 * Load theme data via Ajax and add themes to the section as controls.
   2056 		 *
   2057 		 * @since 4.9.0
   2058 		 *
   2059 		 * @return {void}
   2060 		 */
   2061 		loadThemes: function() {
   2062 			var section = this, params, page, request;
   2063 
   2064 			if ( section.loading ) {
   2065 				return; // We're already loading a batch of themes.
   2066 			}
   2067 
   2068 			// Parameters for every API query. Additional params are set in PHP.
   2069 			page = Math.ceil( section.loaded / 100 ) + 1;
   2070 			params = {
   2071 				'nonce': api.settings.nonce.switch_themes,
   2072 				'wp_customize': 'on',
   2073 				'theme_action': section.params.action,
   2074 				'customized_theme': api.settings.theme.stylesheet,
   2075 				'page': page
   2076 			};
   2077 
   2078 			// Add fields for remote filtering.
   2079 			if ( 'remote' === section.params.filter_type ) {
   2080 				params.search = section.term;
   2081 				params.tags = section.tags;
   2082 			}
   2083 
   2084 			// Load themes.
   2085 			section.headContainer.closest( '.wp-full-overlay' ).addClass( 'loading' );
   2086 			section.loading = true;
   2087 			section.container.find( '.no-themes' ).hide();
   2088 			request = wp.ajax.post( 'customize_load_themes', params );
   2089 			request.done(function( data ) {
   2090 				var themes = data.themes;
   2091 
   2092 				// Stop and try again if the term changed while loading.
   2093 				if ( '' !== section.nextTerm || '' !== section.nextTags ) {
   2094 					if ( section.nextTerm ) {
   2095 						section.term = section.nextTerm;
   2096 					}
   2097 					if ( section.nextTags ) {
   2098 						section.tags = section.nextTags;
   2099 					}
   2100 					section.nextTerm = '';
   2101 					section.nextTags = '';
   2102 					section.loading = false;
   2103 					section.loadThemes();
   2104 					return;
   2105 				}
   2106 
   2107 				if ( 0 !== themes.length ) {
   2108 
   2109 					section.loadControls( themes, page );
   2110 
   2111 					if ( 1 === page ) {
   2112 
   2113 						// Pre-load the first 3 theme screenshots.
   2114 						_.each( section.controls().slice( 0, 3 ), function( control ) {
   2115 							var img, src = control.params.theme.screenshot[0];
   2116 							if ( src ) {
   2117 								img = new Image();
   2118 								img.src = src;
   2119 							}
   2120 						});
   2121 						if ( 'local' !== section.params.filter_type ) {
   2122 							wp.a11y.speak( api.settings.l10n.themeSearchResults.replace( '%d', data.info.results ) );
   2123 						}
   2124 					}
   2125 
   2126 					_.delay( section.renderScreenshots, 100 ); // Wait for the controls to become visible.
   2127 
   2128 					if ( 'local' === section.params.filter_type || 100 > themes.length ) {
   2129 						// If we have less than the requested 100 themes, it's the end of the list.
   2130 						section.fullyLoaded = true;
   2131 					}
   2132 				} else {
   2133 					if ( 0 === section.loaded ) {
   2134 						section.container.find( '.no-themes' ).show();
   2135 						wp.a11y.speak( section.container.find( '.no-themes' ).text() );
   2136 					} else {
   2137 						section.fullyLoaded = true;
   2138 					}
   2139 				}
   2140 				if ( 'local' === section.params.filter_type ) {
   2141 					section.updateCount(); // Count of visible theme controls.
   2142 				} else {
   2143 					section.updateCount( data.info.results ); // Total number of results including pages not yet loaded.
   2144 				}
   2145 				section.container.find( '.unexpected-error' ).hide(); // Hide error notice in case it was previously shown.
   2146 
   2147 				// This cannot run on request.always, as section.loading may turn false before the new controls load in the success case.
   2148 				section.headContainer.closest( '.wp-full-overlay' ).removeClass( 'loading' );
   2149 				section.loading = false;
   2150 			});
   2151 			request.fail(function( data ) {
   2152 				if ( 'undefined' === typeof data ) {
   2153 					section.container.find( '.unexpected-error' ).show();
   2154 					wp.a11y.speak( section.container.find( '.unexpected-error' ).text() );
   2155 				} else if ( 'undefined' !== typeof console && console.error ) {
   2156 					console.error( data );
   2157 				}
   2158 
   2159 				// This cannot run on request.always, as section.loading may turn false before the new controls load in the success case.
   2160 				section.headContainer.closest( '.wp-full-overlay' ).removeClass( 'loading' );
   2161 				section.loading = false;
   2162 			});
   2163 		},
   2164 
   2165 		/**
   2166 		 * Loads controls into the section from data received from loadThemes().
   2167 		 *
   2168 		 * @since 4.9.0
   2169 		 * @param {Array}  themes - Array of theme data to create controls with.
   2170 		 * @param {number} page   - Page of results being loaded.
   2171 		 * @return {void}
   2172 		 */
   2173 		loadControls: function( themes, page ) {
   2174 			var newThemeControls = [],
   2175 				section = this;
   2176 
   2177 			// Add controls for each theme.
   2178 			_.each( themes, function( theme ) {
   2179 				var themeControl = new api.controlConstructor.theme( section.params.action + '_theme_' + theme.id, {
   2180 					type: 'theme',
   2181 					section: section.params.id,
   2182 					theme: theme,
   2183 					priority: section.loaded + 1
   2184 				} );
   2185 
   2186 				api.control.add( themeControl );
   2187 				newThemeControls.push( themeControl );
   2188 				section.loaded = section.loaded + 1;
   2189 			});
   2190 
   2191 			if ( 1 !== page ) {
   2192 				Array.prototype.push.apply( section.screenshotQueue, newThemeControls ); // Add new themes to the screenshot queue.
   2193 			}
   2194 		},
   2195 
   2196 		/**
   2197 		 * Determines whether more themes should be loaded, and loads them.
   2198 		 *
   2199 		 * @since 4.9.0
   2200 		 * @return {void}
   2201 		 */
   2202 		loadMore: function() {
   2203 			var section = this, container, bottom, threshold;
   2204 			if ( ! section.fullyLoaded && ! section.loading ) {
   2205 				container = section.container.closest( '.customize-themes-full-container' );
   2206 
   2207 				bottom = container.scrollTop() + container.height();
   2208 				// Use a fixed distance to the bottom of loaded results to avoid unnecessarily
   2209 				// loading results sooner when using a percentage of scroll distance.
   2210 				threshold = container.prop( 'scrollHeight' ) - 3000;
   2211 
   2212 				if ( bottom > threshold ) {
   2213 					section.loadThemes();
   2214 				}
   2215 			}
   2216 		},
   2217 
   2218 		/**
   2219 		 * Event handler for search input that filters visible controls.
   2220 		 *
   2221 		 * @since 4.9.0
   2222 		 *
   2223 		 * @param {string} term - The raw search input value.
   2224 		 * @return {void}
   2225 		 */
   2226 		filterSearch: function( term ) {
   2227 			var count = 0,
   2228 				visible = false,
   2229 				section = this,
   2230 				noFilter = ( api.section.has( 'wporg_themes' ) && 'remote' !== section.params.filter_type ) ? '.no-themes-local' : '.no-themes',
   2231 				controls = section.controls(),
   2232 				terms;
   2233 
   2234 			if ( section.loading ) {
   2235 				return;
   2236 			}
   2237 
   2238 			// Standardize search term format and split into an array of individual words.
   2239 			terms = term.toLowerCase().trim().replace( /-/g, ' ' ).split( ' ' );
   2240 
   2241 			_.each( controls, function( control ) {
   2242 				visible = control.filter( terms ); // Shows/hides and sorts control based on the applicability of the search term.
   2243 				if ( visible ) {
   2244 					count = count + 1;
   2245 				}
   2246 			});
   2247 
   2248 			if ( 0 === count ) {
   2249 				section.container.find( noFilter ).show();
   2250 				wp.a11y.speak( section.container.find( noFilter ).text() );
   2251 			} else {
   2252 				section.container.find( noFilter ).hide();
   2253 			}
   2254 
   2255 			section.renderScreenshots();
   2256 			api.reflowPaneContents();
   2257 
   2258 			// Update theme count.
   2259 			section.updateCountDebounced( count );
   2260 		},
   2261 
   2262 		/**
   2263 		 * Event handler for search input that determines if the terms have changed and loads new controls as needed.
   2264 		 *
   2265 		 * @since 4.9.0
   2266 		 *
   2267 		 * @param {wp.customize.ThemesSection} section - The current theme section, passed through the debouncer.
   2268 		 * @return {void}
   2269 		 */
   2270 		checkTerm: function( section ) {
   2271 			var newTerm;
   2272 			if ( 'remote' === section.params.filter_type ) {
   2273 				newTerm = section.contentContainer.find( '.wp-filter-search' ).val();
   2274 				if ( section.term !== newTerm.trim() ) {
   2275 					section.initializeNewQuery( newTerm, section.tags );
   2276 				}
   2277 			}
   2278 		},
   2279 
   2280 		/**
   2281 		 * Check for filters checked in the feature filter list and initialize a new query.
   2282 		 *
   2283 		 * @since 4.9.0
   2284 		 *
   2285 		 * @return {void}
   2286 		 */
   2287 		filtersChecked: function() {
   2288 			var section = this,
   2289 			    items = section.container.find( '.filter-group' ).find( ':checkbox' ),
   2290 			    tags = [];
   2291 
   2292 			_.each( items.filter( ':checked' ), function( item ) {
   2293 				tags.push( $( item ).prop( 'value' ) );
   2294 			});
   2295 
   2296 			// When no filters are checked, restore initial state. Update filter count.
   2297 			if ( 0 === tags.length ) {
   2298 				tags = '';
   2299 				section.contentContainer.find( '.feature-filter-toggle .filter-count-0' ).show();
   2300 				section.contentContainer.find( '.feature-filter-toggle .filter-count-filters' ).hide();
   2301 			} else {
   2302 				section.contentContainer.find( '.feature-filter-toggle .theme-filter-count' ).text( tags.length );
   2303 				section.contentContainer.find( '.feature-filter-toggle .filter-count-0' ).hide();
   2304 				section.contentContainer.find( '.feature-filter-toggle .filter-count-filters' ).show();
   2305 			}
   2306 
   2307 			// Check whether tags have changed, and either load or queue them.
   2308 			if ( ! _.isEqual( section.tags, tags ) ) {
   2309 				if ( section.loading ) {
   2310 					section.nextTags = tags;
   2311 				} else {
   2312 					if ( 'remote' === section.params.filter_type ) {
   2313 						section.initializeNewQuery( section.term, tags );
   2314 					} else if ( 'local' === section.params.filter_type ) {
   2315 						section.filterSearch( tags.join( ' ' ) );
   2316 					}
   2317 				}
   2318 			}
   2319 		},
   2320 
   2321 		/**
   2322 		 * Reset the current query and load new results.
   2323 		 *
   2324 		 * @since 4.9.0
   2325 		 *
   2326 		 * @param {string} newTerm - New term.
   2327 		 * @param {Array} newTags - New tags.
   2328 		 * @return {void}
   2329 		 */
   2330 		initializeNewQuery: function( newTerm, newTags ) {
   2331 			var section = this;
   2332 
   2333 			// Clear the controls in the section.
   2334 			_.each( section.controls(), function( control ) {
   2335 				control.container.remove();
   2336 				api.control.remove( control.id );
   2337 			});
   2338 			section.loaded = 0;
   2339 			section.fullyLoaded = false;
   2340 			section.screenshotQueue = null;
   2341 
   2342 			// Run a new query, with loadThemes handling paging, etc.
   2343 			if ( ! section.loading ) {
   2344 				section.term = newTerm;
   2345 				section.tags = newTags;
   2346 				section.loadThemes();
   2347 			} else {
   2348 				section.nextTerm = newTerm; // This will reload from loadThemes() with the newest term once the current batch is loaded.
   2349 				section.nextTags = newTags; // This will reload from loadThemes() with the newest tags once the current batch is loaded.
   2350 			}
   2351 			if ( ! section.expanded() ) {
   2352 				section.expand(); // Expand the section if it isn't expanded.
   2353 			}
   2354 		},
   2355 
   2356 		/**
   2357 		 * Render control's screenshot if the control comes into view.
   2358 		 *
   2359 		 * @since 4.2.0
   2360 		 *
   2361 		 * @return {void}
   2362 		 */
   2363 		renderScreenshots: function() {
   2364 			var section = this;
   2365 
   2366 			// Fill queue initially, or check for more if empty.
   2367 			if ( null === section.screenshotQueue || 0 === section.screenshotQueue.length ) {
   2368 
   2369 				// Add controls that haven't had their screenshots rendered.
   2370 				section.screenshotQueue = _.filter( section.controls(), function( control ) {
   2371 					return ! control.screenshotRendered;
   2372 				});
   2373 			}
   2374 
   2375 			// Are all screenshots rendered (for now)?
   2376 			if ( ! section.screenshotQueue.length ) {
   2377 				return;
   2378 			}
   2379 
   2380 			section.screenshotQueue = _.filter( section.screenshotQueue, function( control ) {
   2381 				var $imageWrapper = control.container.find( '.theme-screenshot' ),
   2382 					$image = $imageWrapper.find( 'img' );
   2383 
   2384 				if ( ! $image.length ) {
   2385 					return false;
   2386 				}
   2387 
   2388 				if ( $image.is( ':hidden' ) ) {
   2389 					return true;
   2390 				}
   2391 
   2392 				// Based on unveil.js.
   2393 				var wt = section.$window.scrollTop(),
   2394 					wb = wt + section.$window.height(),
   2395 					et = $image.offset().top,
   2396 					ih = $imageWrapper.height(),
   2397 					eb = et + ih,
   2398 					threshold = ih * 3,
   2399 					inView = eb >= wt - threshold && et <= wb + threshold;
   2400 
   2401 				if ( inView ) {
   2402 					control.container.trigger( 'render-screenshot' );
   2403 				}
   2404 
   2405 				// If the image is in view return false so it's cleared from the queue.
   2406 				return ! inView;
   2407 			} );
   2408 		},
   2409 
   2410 		/**
   2411 		 * Get visible count.
   2412 		 *
   2413 		 * @since 4.9.0
   2414 		 *
   2415 		 * @return {number} Visible count.
   2416 		 */
   2417 		getVisibleCount: function() {
   2418 			return this.contentContainer.find( 'li.customize-control:visible' ).length;
   2419 		},
   2420 
   2421 		/**
   2422 		 * Update the number of themes in the section.
   2423 		 *
   2424 		 * @since 4.9.0
   2425 		 *
   2426 		 * @return {void}
   2427 		 */
   2428 		updateCount: function( count ) {
   2429 			var section = this, countEl, displayed;
   2430 
   2431 			if ( ! count && 0 !== count ) {
   2432 				count = section.getVisibleCount();
   2433 			}
   2434 
   2435 			displayed = section.contentContainer.find( '.themes-displayed' );
   2436 			countEl = section.contentContainer.find( '.theme-count' );
   2437 
   2438 			if ( 0 === count ) {
   2439 				countEl.text( '0' );
   2440 			} else {
   2441 
   2442 				// Animate the count change for emphasis.
   2443 				displayed.fadeOut( 180, function() {
   2444 					countEl.text( count );
   2445 					displayed.fadeIn( 180 );
   2446 				} );
   2447 				wp.a11y.speak( api.settings.l10n.announceThemeCount.replace( '%d', count ) );
   2448 			}
   2449 		},
   2450 
   2451 		/**
   2452 		 * Advance the modal to the next theme.
   2453 		 *
   2454 		 * @since 4.2.0
   2455 		 *
   2456 		 * @return {void}
   2457 		 */
   2458 		nextTheme: function () {
   2459 			var section = this;
   2460 			if ( section.getNextTheme() ) {
   2461 				section.showDetails( section.getNextTheme(), function() {
   2462 					section.overlay.find( '.right' ).focus();
   2463 				} );
   2464 			}
   2465 		},
   2466 
   2467 		/**
   2468 		 * Get the next theme model.
   2469 		 *
   2470 		 * @since 4.2.0
   2471 		 *
   2472 		 * @return {wp.customize.ThemeControl|boolean} Next theme.
   2473 		 */
   2474 		getNextTheme: function () {
   2475 			var section = this, control, nextControl, sectionControls, i;
   2476 			control = api.control( section.params.action + '_theme_' + section.currentTheme );
   2477 			sectionControls = section.controls();
   2478 			i = _.indexOf( sectionControls, control );
   2479 			if ( -1 === i ) {
   2480 				return false;
   2481 			}
   2482 
   2483 			nextControl = sectionControls[ i + 1 ];
   2484 			if ( ! nextControl ) {
   2485 				return false;
   2486 			}
   2487 			return nextControl.params.theme;
   2488 		},
   2489 
   2490 		/**
   2491 		 * Advance the modal to the previous theme.
   2492 		 *
   2493 		 * @since 4.2.0
   2494 		 * @return {void}
   2495 		 */
   2496 		previousTheme: function () {
   2497 			var section = this;
   2498 			if ( section.getPreviousTheme() ) {
   2499 				section.showDetails( section.getPreviousTheme(), function() {
   2500 					section.overlay.find( '.left' ).focus();
   2501 				} );
   2502 			}
   2503 		},
   2504 
   2505 		/**
   2506 		 * Get the previous theme model.
   2507 		 *
   2508 		 * @since 4.2.0
   2509 		 * @return {wp.customize.ThemeControl|boolean} Previous theme.
   2510 		 */
   2511 		getPreviousTheme: function () {
   2512 			var section = this, control, nextControl, sectionControls, i;
   2513 			control = api.control( section.params.action + '_theme_' + section.currentTheme );
   2514 			sectionControls = section.controls();
   2515 			i = _.indexOf( sectionControls, control );
   2516 			if ( -1 === i ) {
   2517 				return false;
   2518 			}
   2519 
   2520 			nextControl = sectionControls[ i - 1 ];
   2521 			if ( ! nextControl ) {
   2522 				return false;
   2523 			}
   2524 			return nextControl.params.theme;
   2525 		},
   2526 
   2527 		/**
   2528 		 * Disable buttons when we're viewing the first or last theme.
   2529 		 *
   2530 		 * @since 4.2.0
   2531 		 *
   2532 		 * @return {void}
   2533 		 */
   2534 		updateLimits: function () {
   2535 			if ( ! this.getNextTheme() ) {
   2536 				this.overlay.find( '.right' ).addClass( 'disabled' );
   2537 			}
   2538 			if ( ! this.getPreviousTheme() ) {
   2539 				this.overlay.find( '.left' ).addClass( 'disabled' );
   2540 			}
   2541 		},
   2542 
   2543 		/**
   2544 		 * Load theme preview.
   2545 		 *
   2546 		 * @since 4.7.0
   2547 		 * @access public
   2548 		 *
   2549 		 * @deprecated
   2550 		 * @param {string} themeId Theme ID.
   2551 		 * @return {jQuery.promise} Promise.
   2552 		 */
   2553 		loadThemePreview: function( themeId ) {
   2554 			return api.ThemesPanel.prototype.loadThemePreview.call( this, themeId );
   2555 		},
   2556 
   2557 		/**
   2558 		 * Render & show the theme details for a given theme model.
   2559 		 *
   2560 		 * @since 4.2.0
   2561 		 *
   2562 		 * @param {Object} theme - Theme.
   2563 		 * @param {Function} [callback] - Callback once the details have been shown.
   2564 		 * @return {void}
   2565 		 */
   2566 		showDetails: function ( theme, callback ) {
   2567 			var section = this, panel = api.panel( 'themes' );
   2568 			section.currentTheme = theme.id;
   2569 			section.overlay.html( section.template( theme ) )
   2570 				.fadeIn( 'fast' )
   2571 				.focus();
   2572 
   2573 			function disableSwitchButtons() {
   2574 				return ! panel.canSwitchTheme( theme.id );
   2575 			}
   2576 
   2577 			// Temporary special function since supplying SFTP credentials does not work yet. See #42184.
   2578 			function disableInstallButtons() {
   2579 				return disableSwitchButtons() || false === api.settings.theme._canInstall || true === api.settings.theme._filesystemCredentialsNeeded;
   2580 			}
   2581 
   2582 			section.overlay.find( 'button.preview, button.preview-theme' ).toggleClass( 'disabled', disableSwitchButtons() );
   2583 			section.overlay.find( 'button.theme-install' ).toggleClass( 'disabled', disableInstallButtons() );
   2584 
   2585 			section.$body.addClass( 'modal-open' );
   2586 			section.containFocus( section.overlay );
   2587 			section.updateLimits();
   2588 			wp.a11y.speak( api.settings.l10n.announceThemeDetails.replace( '%s', theme.name ) );
   2589 			if ( callback ) {
   2590 				callback();
   2591 			}
   2592 		},
   2593 
   2594 		/**
   2595 		 * Close the theme details modal.
   2596 		 *
   2597 		 * @since 4.2.0
   2598 		 *
   2599 		 * @return {void}
   2600 		 */
   2601 		closeDetails: function () {
   2602 			var section = this;
   2603 			section.$body.removeClass( 'modal-open' );
   2604 			section.overlay.fadeOut( 'fast' );
   2605 			api.control( section.params.action + '_theme_' + section.currentTheme ).container.find( '.theme' ).focus();
   2606 		},
   2607 
   2608 		/**
   2609 		 * Keep tab focus within the theme details modal.
   2610 		 *
   2611 		 * @since 4.2.0
   2612 		 *
   2613 		 * @param {jQuery} el - Element to contain focus.
   2614 		 * @return {void}
   2615 		 */
   2616 		containFocus: function( el ) {
   2617 			var tabbables;
   2618 
   2619 			el.on( 'keydown', function( event ) {
   2620 
   2621 				// Return if it's not the tab key
   2622 				// When navigating with prev/next focus is already handled.
   2623 				if ( 9 !== event.keyCode ) {
   2624 					return;
   2625 				}
   2626 
   2627 				// Uses jQuery UI to get the tabbable elements.
   2628 				tabbables = $( ':tabbable', el );
   2629 
   2630 				// Keep focus within the overlay.
   2631 				if ( tabbables.last()[0] === event.target && ! event.shiftKey ) {
   2632 					tabbables.first().focus();
   2633 					return false;
   2634 				} else if ( tabbables.first()[0] === event.target && event.shiftKey ) {
   2635 					tabbables.last().focus();
   2636 					return false;
   2637 				}
   2638 			});
   2639 		}
   2640 	});
   2641 
   2642 	api.OuterSection = api.Section.extend(/** @lends wp.customize.OuterSection.prototype */{
   2643 
   2644 		/**
   2645 		 * Class wp.customize.OuterSection.
   2646 		 *
   2647 		 * Creates section outside of the sidebar, there is no ui to trigger collapse/expand so
   2648 		 * it would require custom handling.
   2649 		 *
   2650 		 * @constructs wp.customize.OuterSection
   2651 		 * @augments   wp.customize.Section
   2652 		 *
   2653 		 * @since 4.9.0
   2654 		 *
   2655 		 * @return {void}
   2656 		 */
   2657 		initialize: function() {
   2658 			var section = this;
   2659 			section.containerParent = '#customize-outer-theme-controls';
   2660 			section.containerPaneParent = '.customize-outer-pane-parent';
   2661 			api.Section.prototype.initialize.apply( section, arguments );
   2662 		},
   2663 
   2664 		/**
   2665 		 * Overrides api.Section.prototype.onChangeExpanded to prevent collapse/expand effect
   2666 		 * on other sections and panels.
   2667 		 *
   2668 		 * @since 4.9.0
   2669 		 *
   2670 		 * @param {boolean}  expanded - The expanded state to transition to.
   2671 		 * @param {Object}   [args] - Args.
   2672 		 * @param {boolean}  [args.unchanged] - Whether the state is already known to not be changed, and so short-circuit with calling completeCallback early.
   2673 		 * @param {Function} [args.completeCallback] - Function to call when the slideUp/slideDown has completed.
   2674 		 * @param {Object}   [args.duration] - The duration for the animation.
   2675 		 */
   2676 		onChangeExpanded: function( expanded, args ) {
   2677 			var section = this,
   2678 				container = section.headContainer.closest( '.wp-full-overlay-sidebar-content' ),
   2679 				content = section.contentContainer,
   2680 				backBtn = content.find( '.customize-section-back' ),
   2681 				sectionTitle = section.headContainer.find( '.accordion-section-title' ).first(),
   2682 				body = $( document.body ),
   2683 				expand, panel;
   2684 
   2685 			body.toggleClass( 'outer-section-open', expanded );
   2686 			section.container.toggleClass( 'open', expanded );
   2687 			section.container.removeClass( 'busy' );
   2688 			api.section.each( function( _section ) {
   2689 				if ( 'outer' === _section.params.type && _section.id !== section.id ) {
   2690 					_section.container.removeClass( 'open' );
   2691 				}
   2692 			} );
   2693 
   2694 			if ( expanded && ! content.hasClass( 'open' ) ) {
   2695 
   2696 				if ( args.unchanged ) {
   2697 					expand = args.completeCallback;
   2698 				} else {
   2699 					expand = function() {
   2700 						section._animateChangeExpanded( function() {
   2701 							sectionTitle.attr( 'tabindex', '-1' );
   2702 							backBtn.attr( 'tabindex', '0' );
   2703 
   2704 							backBtn.trigger( 'focus' );
   2705 							content.css( 'top', '' );
   2706 							container.scrollTop( 0 );
   2707 
   2708 							if ( args.completeCallback ) {
   2709 								args.completeCallback();
   2710 							}
   2711 						} );
   2712 
   2713 						content.addClass( 'open' );
   2714 					}.bind( this );
   2715 				}
   2716 
   2717 				if ( section.panel() ) {
   2718 					api.panel( section.panel() ).expand({
   2719 						duration: args.duration,
   2720 						completeCallback: expand
   2721 					});
   2722 				} else {
   2723 					expand();
   2724 				}
   2725 
   2726 			} else if ( ! expanded && content.hasClass( 'open' ) ) {
   2727 				if ( section.panel() ) {
   2728 					panel = api.panel( section.panel() );
   2729 					if ( panel.contentContainer.hasClass( 'skip-transition' ) ) {
   2730 						panel.collapse();
   2731 					}
   2732 				}
   2733 				section._animateChangeExpanded( function() {
   2734 					backBtn.attr( 'tabindex', '-1' );
   2735 					sectionTitle.attr( 'tabindex', '0' );
   2736 
   2737 					sectionTitle.trigger( 'focus' );
   2738 					content.css( 'top', '' );
   2739 
   2740 					if ( args.completeCallback ) {
   2741 						args.completeCallback();
   2742 					}
   2743 				} );
   2744 
   2745 				content.removeClass( 'open' );
   2746 
   2747 			} else {
   2748 				if ( args.completeCallback ) {
   2749 					args.completeCallback();
   2750 				}
   2751 			}
   2752 		}
   2753 	});
   2754 
   2755 	api.Panel = Container.extend(/** @lends wp.customize.Panel.prototype */{
   2756 		containerType: 'panel',
   2757 
   2758 		/**
   2759 		 * @constructs wp.customize.Panel
   2760 		 * @augments   wp.customize~Container
   2761 		 *
   2762 		 * @since 4.1.0
   2763 		 *
   2764 		 * @param {string}  id - The ID for the panel.
   2765 		 * @param {Object}  options - Object containing one property: params.
   2766 		 * @param {string}  options.title - Title shown when panel is collapsed and expanded.
   2767 		 * @param {string}  [options.description] - Description shown at the top of the panel.
   2768 		 * @param {number}  [options.priority=100] - The sort priority for the panel.
   2769 		 * @param {string}  [options.type=default] - The type of the panel. See wp.customize.panelConstructor.
   2770 		 * @param {string}  [options.content] - The markup to be used for the panel container. If empty, a JS template is used.
   2771 		 * @param {boolean} [options.active=true] - Whether the panel is active or not.
   2772 		 * @param {Object}  [options.params] - Deprecated wrapper for the above properties.
   2773 		 */
   2774 		initialize: function ( id, options ) {
   2775 			var panel = this, params;
   2776 			params = options.params || options;
   2777 
   2778 			// Look up the type if one was not supplied.
   2779 			if ( ! params.type ) {
   2780 				_.find( api.panelConstructor, function( Constructor, type ) {
   2781 					if ( Constructor === panel.constructor ) {
   2782 						params.type = type;
   2783 						return true;
   2784 					}
   2785 					return false;
   2786 				} );
   2787 			}
   2788 
   2789 			Container.prototype.initialize.call( panel, id, params );
   2790 
   2791 			panel.embed();
   2792 			panel.deferred.embedded.done( function () {
   2793 				panel.ready();
   2794 			});
   2795 		},
   2796 
   2797 		/**
   2798 		 * Embed the container in the DOM when any parent panel is ready.
   2799 		 *
   2800 		 * @since 4.1.0
   2801 		 */
   2802 		embed: function () {
   2803 			var panel = this,
   2804 				container = $( '#customize-theme-controls' ),
   2805 				parentContainer = $( '.customize-pane-parent' ); // @todo This should be defined elsewhere, and to be configurable.
   2806 
   2807 			if ( ! panel.headContainer.parent().is( parentContainer ) ) {
   2808 				parentContainer.append( panel.headContainer );
   2809 			}
   2810 			if ( ! panel.contentContainer.parent().is( panel.headContainer ) ) {
   2811 				container.append( panel.contentContainer );
   2812 			}
   2813 			panel.renderContent();
   2814 
   2815 			panel.deferred.embedded.resolve();
   2816 		},
   2817 
   2818 		/**
   2819 		 * @since 4.1.0
   2820 		 */
   2821 		attachEvents: function () {
   2822 			var meta, panel = this;
   2823 
   2824 			// Expand/Collapse accordion sections on click.
   2825 			panel.headContainer.find( '.accordion-section-title' ).on( 'click keydown', function( event ) {
   2826 				if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
   2827 					return;
   2828 				}
   2829 				event.preventDefault(); // Keep this AFTER the key filter above.
   2830 
   2831 				if ( ! panel.expanded() ) {
   2832 					panel.expand();
   2833 				}
   2834 			});
   2835 
   2836 			// Close panel.
   2837 			panel.container.find( '.customize-panel-back' ).on( 'click keydown', function( event ) {
   2838 				if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
   2839 					return;
   2840 				}
   2841 				event.preventDefault(); // Keep this AFTER the key filter above.
   2842 
   2843 				if ( panel.expanded() ) {
   2844 					panel.collapse();
   2845 				}
   2846 			});
   2847 
   2848 			meta = panel.container.find( '.panel-meta:first' );
   2849 
   2850 			meta.find( '> .accordion-section-title .customize-help-toggle' ).on( 'click', function() {
   2851 				if ( meta.hasClass( 'cannot-expand' ) ) {
   2852 					return;
   2853 				}
   2854 
   2855 				var content = meta.find( '.customize-panel-description:first' );
   2856 				if ( meta.hasClass( 'open' ) ) {
   2857 					meta.toggleClass( 'open' );
   2858 					content.slideUp( panel.defaultExpandedArguments.duration, function() {
   2859 						content.trigger( 'toggled' );
   2860 					} );
   2861 					$( this ).attr( 'aria-expanded', false );
   2862 				} else {
   2863 					content.slideDown( panel.defaultExpandedArguments.duration, function() {
   2864 						content.trigger( 'toggled' );
   2865 					} );
   2866 					meta.toggleClass( 'open' );
   2867 					$( this ).attr( 'aria-expanded', true );
   2868 				}
   2869 			});
   2870 
   2871 		},
   2872 
   2873 		/**
   2874 		 * Get the sections that are associated with this panel, sorted by their priority Value.
   2875 		 *
   2876 		 * @since 4.1.0
   2877 		 *
   2878 		 * @return {Array}
   2879 		 */
   2880 		sections: function () {
   2881 			return this._children( 'panel', 'section' );
   2882 		},
   2883 
   2884 		/**
   2885 		 * Return whether this panel has any active sections.
   2886 		 *
   2887 		 * @since 4.1.0
   2888 		 *
   2889 		 * @return {boolean} Whether contextually active.
   2890 		 */
   2891 		isContextuallyActive: function () {
   2892 			var panel = this,
   2893 				sections = panel.sections(),
   2894 				activeCount = 0;
   2895 			_( sections ).each( function ( section ) {
   2896 				if ( section.active() && section.isContextuallyActive() ) {
   2897 					activeCount += 1;
   2898 				}
   2899 			} );
   2900 			return ( activeCount !== 0 );
   2901 		},
   2902 
   2903 		/**
   2904 		 * Update UI to reflect expanded state.
   2905 		 *
   2906 		 * @since 4.1.0
   2907 		 *
   2908 		 * @param {boolean}  expanded
   2909 		 * @param {Object}   args
   2910 		 * @param {boolean}  args.unchanged
   2911 		 * @param {Function} args.completeCallback
   2912 		 * @return {void}
   2913 		 */
   2914 		onChangeExpanded: function ( expanded, args ) {
   2915 
   2916 			// Immediately call the complete callback if there were no changes.
   2917 			if ( args.unchanged ) {
   2918 				if ( args.completeCallback ) {
   2919 					args.completeCallback();
   2920 				}
   2921 				return;
   2922 			}
   2923 
   2924 			// Note: there is a second argument 'args' passed.
   2925 			var panel = this,
   2926 				accordionSection = panel.contentContainer,
   2927 				overlay = accordionSection.closest( '.wp-full-overlay' ),
   2928 				container = accordionSection.closest( '.wp-full-overlay-sidebar-content' ),
   2929 				topPanel = panel.headContainer.find( '.accordion-section-title' ),
   2930 				backBtn = accordionSection.find( '.customize-panel-back' ),
   2931 				childSections = panel.sections(),
   2932 				skipTransition;
   2933 
   2934 			if ( expanded && ! accordionSection.hasClass( 'current-panel' ) ) {
   2935 				// Collapse any sibling sections/panels.
   2936 				api.section.each( function ( section ) {
   2937 					if ( panel.id !== section.panel() ) {
   2938 						section.collapse( { duration: 0 } );
   2939 					}
   2940 				});
   2941 				api.panel.each( function ( otherPanel ) {
   2942 					if ( panel !== otherPanel ) {
   2943 						otherPanel.collapse( { duration: 0 } );
   2944 					}
   2945 				});
   2946 
   2947 				if ( panel.params.autoExpandSoleSection && 1 === childSections.length && childSections[0].active.get() ) {
   2948 					accordionSection.addClass( 'current-panel skip-transition' );
   2949 					overlay.addClass( 'in-sub-panel' );
   2950 
   2951 					childSections[0].expand( {
   2952 						completeCallback: args.completeCallback
   2953 					} );
   2954 				} else {
   2955 					panel._animateChangeExpanded( function() {
   2956 						topPanel.attr( 'tabindex', '-1' );
   2957 						backBtn.attr( 'tabindex', '0' );
   2958 
   2959 						backBtn.trigger( 'focus' );
   2960 						accordionSection.css( 'top', '' );
   2961 						container.scrollTop( 0 );
   2962 
   2963 						if ( args.completeCallback ) {
   2964 							args.completeCallback();
   2965 						}
   2966 					} );
   2967 
   2968 					accordionSection.addClass( 'current-panel' );
   2969 					overlay.addClass( 'in-sub-panel' );
   2970 				}
   2971 
   2972 				api.state( 'expandedPanel' ).set( panel );
   2973 
   2974 			} else if ( ! expanded && accordionSection.hasClass( 'current-panel' ) ) {
   2975 				skipTransition = accordionSection.hasClass( 'skip-transition' );
   2976 				if ( ! skipTransition ) {
   2977 					panel._animateChangeExpanded( function() {
   2978 						topPanel.attr( 'tabindex', '0' );
   2979 						backBtn.attr( 'tabindex', '-1' );
   2980 
   2981 						topPanel.focus();
   2982 						accordionSection.css( 'top', '' );
   2983 
   2984 						if ( args.completeCallback ) {
   2985 							args.completeCallback();
   2986 						}
   2987 					} );
   2988 				} else {
   2989 					accordionSection.removeClass( 'skip-transition' );
   2990 				}
   2991 
   2992 				overlay.removeClass( 'in-sub-panel' );
   2993 				accordionSection.removeClass( 'current-panel' );
   2994 				if ( panel === api.state( 'expandedPanel' ).get() ) {
   2995 					api.state( 'expandedPanel' ).set( false );
   2996 				}
   2997 			}
   2998 		},
   2999 
   3000 		/**
   3001 		 * Render the panel from its JS template, if it exists.
   3002 		 *
   3003 		 * The panel's container must already exist in the DOM.
   3004 		 *
   3005 		 * @since 4.3.0
   3006 		 */
   3007 		renderContent: function () {
   3008 			var template,
   3009 				panel = this;
   3010 
   3011 			// Add the content to the container.
   3012 			if ( 0 !== $( '#tmpl-' + panel.templateSelector + '-content' ).length ) {
   3013 				template = wp.template( panel.templateSelector + '-content' );
   3014 			} else {
   3015 				template = wp.template( 'customize-panel-default-content' );
   3016 			}
   3017 			if ( template && panel.headContainer ) {
   3018 				panel.contentContainer.html( template( _.extend(
   3019 					{ id: panel.id },
   3020 					panel.params
   3021 				) ) );
   3022 			}
   3023 		}
   3024 	});
   3025 
   3026 	api.ThemesPanel = api.Panel.extend(/** @lends wp.customize.ThemsPanel.prototype */{
   3027 
   3028 		/**
   3029 		 *  Class wp.customize.ThemesPanel.
   3030 		 *
   3031 		 * Custom section for themes that displays without the customize preview.
   3032 		 *
   3033 		 * @constructs wp.customize.ThemesPanel
   3034 		 * @augments   wp.customize.Panel
   3035 		 *
   3036 		 * @since 4.9.0
   3037 		 *
   3038 		 * @param {string} id - The ID for the panel.
   3039 		 * @param {Object} options - Options.
   3040 		 * @return {void}
   3041 		 */
   3042 		initialize: function( id, options ) {
   3043 			var panel = this;
   3044 			panel.installingThemes = [];
   3045 			api.Panel.prototype.initialize.call( panel, id, options );
   3046 		},
   3047 
   3048 		/**
   3049 		 * Determine whether a given theme can be switched to, or in general.
   3050 		 *
   3051 		 * @since 4.9.0
   3052 		 *
   3053 		 * @param {string} [slug] - Theme slug.
   3054 		 * @return {boolean} Whether the theme can be switched to.
   3055 		 */
   3056 		canSwitchTheme: function canSwitchTheme( slug ) {
   3057 			if ( slug && slug === api.settings.theme.stylesheet ) {
   3058 				return true;
   3059 			}
   3060 			return 'publish' === api.state( 'selectedChangesetStatus' ).get() && ( '' === api.state( 'changesetStatus' ).get() || 'auto-draft' === api.state( 'changesetStatus' ).get() );
   3061 		},
   3062 
   3063 		/**
   3064 		 * Attach events.
   3065 		 *
   3066 		 * @since 4.9.0
   3067 		 * @return {void}
   3068 		 */
   3069 		attachEvents: function() {
   3070 			var panel = this;
   3071 
   3072 			// Attach regular panel events.
   3073 			api.Panel.prototype.attachEvents.apply( panel );
   3074 
   3075 			// Temporary since supplying SFTP credentials does not work yet. See #42184.
   3076 			if ( api.settings.theme._canInstall && api.settings.theme._filesystemCredentialsNeeded ) {
   3077 				panel.notifications.add( new api.Notification( 'theme_install_unavailable', {
   3078 					message: api.l10n.themeInstallUnavailable,
   3079 					type: 'info',
   3080 					dismissible: true
   3081 				} ) );
   3082 			}
   3083 
   3084 			function toggleDisabledNotifications() {
   3085 				if ( panel.canSwitchTheme() ) {
   3086 					panel.notifications.remove( 'theme_switch_unavailable' );
   3087 				} else {
   3088 					panel.notifications.add( new api.Notification( 'theme_switch_unavailable', {
   3089 						message: api.l10n.themePreviewUnavailable,
   3090 						type: 'warning'
   3091 					} ) );
   3092 				}
   3093 			}
   3094 			toggleDisabledNotifications();
   3095 			api.state( 'selectedChangesetStatus' ).bind( toggleDisabledNotifications );
   3096 			api.state( 'changesetStatus' ).bind( toggleDisabledNotifications );
   3097 
   3098 			// Collapse panel to customize the current theme.
   3099 			panel.contentContainer.on( 'click', '.customize-theme', function() {
   3100 				panel.collapse();
   3101 			});
   3102 
   3103 			// Toggle between filtering and browsing themes on mobile.
   3104 			panel.contentContainer.on( 'click', '.customize-themes-section-title, .customize-themes-mobile-back', function() {
   3105 				$( '.wp-full-overlay' ).toggleClass( 'showing-themes' );
   3106 			});
   3107 
   3108 			// Install (and maybe preview) a theme.
   3109 			panel.contentContainer.on( 'click', '.theme-install', function( event ) {
   3110 				panel.installTheme( event );
   3111 			});
   3112 
   3113 			// Update a theme. Theme cards have the class, the details modal has the id.
   3114 			panel.contentContainer.on( 'click', '.update-theme, #update-theme', function( event ) {
   3115 
   3116 				// #update-theme is a link.
   3117 				event.preventDefault();
   3118 				event.stopPropagation();
   3119 
   3120 				panel.updateTheme( event );
   3121 			});
   3122 
   3123 			// Delete a theme.
   3124 			panel.contentContainer.on( 'click', '.delete-theme', function( event ) {
   3125 				panel.deleteTheme( event );
   3126 			});
   3127 
   3128 			_.bindAll( panel, 'installTheme', 'updateTheme' );
   3129 		},
   3130 
   3131 		/**
   3132 		 * Update UI to reflect expanded state
   3133 		 *
   3134 		 * @since 4.9.0
   3135 		 *
   3136 		 * @param {boolean}  expanded - Expanded state.
   3137 		 * @param {Object}   args - Args.
   3138 		 * @param {boolean}  args.unchanged - Whether or not the state changed.
   3139 		 * @param {Function} args.completeCallback - Callback to execute when the animation completes.
   3140 		 * @return {void}
   3141 		 */
   3142 		onChangeExpanded: function( expanded, args ) {
   3143 			var panel = this, overlay, sections, hasExpandedSection = false;
   3144 
   3145 			// Expand/collapse the panel normally.
   3146 			api.Panel.prototype.onChangeExpanded.apply( this, [ expanded, args ] );
   3147 
   3148 			// Immediately call the complete callback if there were no changes.
   3149 			if ( args.unchanged ) {
   3150 				if ( args.completeCallback ) {
   3151 					args.completeCallback();
   3152 				}
   3153 				return;
   3154 			}
   3155 
   3156 			overlay = panel.headContainer.closest( '.wp-full-overlay' );
   3157 
   3158 			if ( expanded ) {
   3159 				overlay
   3160 					.addClass( 'in-themes-panel' )
   3161 					.delay( 200 ).find( '.customize-themes-full-container' ).addClass( 'animate' );
   3162 
   3163 				_.delay( function() {
   3164 					overlay.addClass( 'themes-panel-expanded' );
   3165 				}, 200 );
   3166 
   3167 				// Automatically open the first section (except on small screens), if one isn't already expanded.
   3168 				if ( 600 < window.innerWidth ) {
   3169 					sections = panel.sections();
   3170 					_.each( sections, function( section ) {
   3171 						if ( section.expanded() ) {
   3172 							hasExpandedSection = true;
   3173 						}
   3174 					} );
   3175 					if ( ! hasExpandedSection && sections.length > 0 ) {
   3176 						sections[0].expand();
   3177 					}
   3178 				}
   3179 			} else {
   3180 				overlay
   3181 					.removeClass( 'in-themes-panel themes-panel-expanded' )
   3182 					.find( '.customize-themes-full-container' ).removeClass( 'animate' );
   3183 			}
   3184 		},
   3185 
   3186 		/**
   3187 		 * Install a theme via wp.updates.
   3188 		 *
   3189 		 * @since 4.9.0
   3190 		 *
   3191 		 * @param {jQuery.Event} event - Event.
   3192 		 * @return {jQuery.promise} Promise.
   3193 		 */
   3194 		installTheme: function( event ) {
   3195 			var panel = this, preview, onInstallSuccess, slug = $( event.target ).data( 'slug' ), deferred = $.Deferred(), request;
   3196 			preview = $( event.target ).hasClass( 'preview' );
   3197 
   3198 			// Temporary since supplying SFTP credentials does not work yet. See #42184.
   3199 			if ( api.settings.theme._filesystemCredentialsNeeded ) {
   3200 				deferred.reject({
   3201 					errorCode: 'theme_install_unavailable'
   3202 				});
   3203 				return deferred.promise();
   3204 			}
   3205 
   3206 			// Prevent loading a non-active theme preview when there is a drafted/scheduled changeset.
   3207 			if ( ! panel.canSwitchTheme( slug ) ) {
   3208 				deferred.reject({
   3209 					errorCode: 'theme_switch_unavailable'
   3210 				});
   3211 				return deferred.promise();
   3212 			}
   3213 
   3214 			// Theme is already being installed.
   3215 			if ( _.contains( panel.installingThemes, slug ) ) {
   3216 				deferred.reject({
   3217 					errorCode: 'theme_already_installing'
   3218 				});
   3219 				return deferred.promise();
   3220 			}
   3221 
   3222 			wp.updates.maybeRequestFilesystemCredentials( event );
   3223 
   3224 			onInstallSuccess = function( response ) {
   3225 				var theme = false, themeControl;
   3226 				if ( preview ) {
   3227 					api.notifications.remove( 'theme_installing' );
   3228 
   3229 					panel.loadThemePreview( slug );
   3230 
   3231 				} else {
   3232 					api.control.each( function( control ) {
   3233 						if ( 'theme' === control.params.type && control.params.theme.id === response.slug ) {
   3234 							theme = control.params.theme; // Used below to add theme control.
   3235 							control.rerenderAsInstalled( true );
   3236 						}
   3237 					});
   3238 
   3239 					// Don't add the same theme more than once.
   3240 					if ( ! theme || api.control.has( 'installed_theme_' + theme.id ) ) {
   3241 						deferred.resolve( response );
   3242 						return;
   3243 					}
   3244 
   3245 					// Add theme control to installed section.
   3246 					theme.type = 'installed';
   3247 					themeControl = new api.controlConstructor.theme( 'installed_theme_' + theme.id, {
   3248 						type: 'theme',
   3249 						section: 'installed_themes',
   3250 						theme: theme,
   3251 						priority: 0 // Add all newly-installed themes to the top.
   3252 					} );
   3253 
   3254 					api.control.add( themeControl );
   3255 					api.control( themeControl.id ).container.trigger( 'render-screenshot' );
   3256 
   3257 					// Close the details modal if it's open to the installed theme.
   3258 					api.section.each( function( section ) {
   3259 						if ( 'themes' === section.params.type ) {
   3260 							if ( theme.id === section.currentTheme ) { // Don't close the modal if the user has navigated elsewhere.
   3261 								section.closeDetails();
   3262 							}
   3263 						}
   3264 					});
   3265 				}
   3266 				deferred.resolve( response );
   3267 			};
   3268 
   3269 			panel.installingThemes.push( slug ); // Note: we don't remove elements from installingThemes, since they shouldn't be installed again.
   3270 			request = wp.updates.installTheme( {
   3271 				slug: slug
   3272 			} );
   3273 
   3274 			// Also preview the theme as the event is triggered on Install & Preview.
   3275 			if ( preview ) {
   3276 				api.notifications.add( new api.OverlayNotification( 'theme_installing', {
   3277 					message: api.l10n.themeDownloading,
   3278 					type: 'info',
   3279 					loading: true
   3280 				} ) );
   3281 			}
   3282 
   3283 			request.done( onInstallSuccess );
   3284 			request.fail( function() {
   3285 				api.notifications.remove( 'theme_installing' );
   3286 			} );
   3287 
   3288 			return deferred.promise();
   3289 		},
   3290 
   3291 		/**
   3292 		 * Load theme preview.
   3293 		 *
   3294 		 * @since 4.9.0
   3295 		 *
   3296 		 * @param {string} themeId Theme ID.
   3297 		 * @return {jQuery.promise} Promise.
   3298 		 */
   3299 		loadThemePreview: function( themeId ) {
   3300 			var panel = this, deferred = $.Deferred(), onceProcessingComplete, urlParser, queryParams;
   3301 
   3302 			// Prevent loading a non-active theme preview when there is a drafted/scheduled changeset.
   3303 			if ( ! panel.canSwitchTheme( themeId ) ) {
   3304 				deferred.reject({
   3305 					errorCode: 'theme_switch_unavailable'
   3306 				});
   3307 				return deferred.promise();
   3308 			}
   3309 
   3310 			urlParser = document.createElement( 'a' );
   3311 			urlParser.href = location.href;
   3312 			queryParams = _.extend(
   3313 				api.utils.parseQueryString( urlParser.search.substr( 1 ) ),
   3314 				{
   3315 					theme: themeId,
   3316 					changeset_uuid: api.settings.changeset.uuid,
   3317 					'return': api.settings.url['return']
   3318 				}
   3319 			);
   3320 
   3321 			// Include autosaved param to load autosave revision without prompting user to restore it.
   3322 			if ( ! api.state( 'saved' ).get() ) {
   3323 				queryParams.customize_autosaved = 'on';
   3324 			}
   3325 
   3326 			urlParser.search = $.param( queryParams );
   3327 
   3328 			// Update loading message. Everything else is handled by reloading the page.
   3329 			api.notifications.add( new api.OverlayNotification( 'theme_previewing', {
   3330 				message: api.l10n.themePreviewWait,
   3331 				type: 'info',
   3332 				loading: true
   3333 			} ) );
   3334 
   3335 			onceProcessingComplete = function() {
   3336 				var request;
   3337 				if ( api.state( 'processing' ).get() > 0 ) {
   3338 					return;
   3339 				}
   3340 
   3341 				api.state( 'processing' ).unbind( onceProcessingComplete );
   3342 
   3343 				request = api.requestChangesetUpdate( {}, { autosave: true } );
   3344 				request.done( function() {
   3345 					deferred.resolve();
   3346 					$( window ).off( 'beforeunload.customize-confirm' );
   3347 					location.replace( urlParser.href );
   3348 				} );
   3349 				request.fail( function() {
   3350 
   3351 					// @todo Show notification regarding failure.
   3352 					api.notifications.remove( 'theme_previewing' );
   3353 
   3354 					deferred.reject();
   3355 				} );
   3356 			};
   3357 
   3358 			if ( 0 === api.state( 'processing' ).get() ) {
   3359 				onceProcessingComplete();
   3360 			} else {
   3361 				api.state( 'processing' ).bind( onceProcessingComplete );
   3362 			}
   3363 
   3364 			return deferred.promise();
   3365 		},
   3366 
   3367 		/**
   3368 		 * Update a theme via wp.updates.
   3369 		 *
   3370 		 * @since 4.9.0
   3371 		 *
   3372 		 * @param {jQuery.Event} event - Event.
   3373 		 * @return {void}
   3374 		 */
   3375 		updateTheme: function( event ) {
   3376 			wp.updates.maybeRequestFilesystemCredentials( event );
   3377 
   3378 			$( document ).one( 'wp-theme-update-success', function( e, response ) {
   3379 
   3380 				// Rerender the control to reflect the update.
   3381 				api.control.each( function( control ) {
   3382 					if ( 'theme' === control.params.type && control.params.theme.id === response.slug ) {
   3383 						control.params.theme.hasUpdate = false;
   3384 						control.params.theme.version = response.newVersion;
   3385 						setTimeout( function() {
   3386 							control.rerenderAsInstalled( true );
   3387 						}, 2000 );
   3388 					}
   3389 				});
   3390 			} );
   3391 
   3392 			wp.updates.updateTheme( {
   3393 				slug: $( event.target ).closest( '.notice' ).data( 'slug' )
   3394 			} );
   3395 		},
   3396 
   3397 		/**
   3398 		 * Delete a theme via wp.updates.
   3399 		 *
   3400 		 * @since 4.9.0
   3401 		 *
   3402 		 * @param {jQuery.Event} event - Event.
   3403 		 * @return {void}
   3404 		 */
   3405 		deleteTheme: function( event ) {
   3406 			var theme, section;
   3407 			theme = $( event.target ).data( 'slug' );
   3408 			section = api.section( 'installed_themes' );
   3409 
   3410 			event.preventDefault();
   3411 
   3412 			// Temporary since supplying SFTP credentials does not work yet. See #42184.
   3413 			if ( api.settings.theme._filesystemCredentialsNeeded ) {
   3414 				return;
   3415 			}
   3416 
   3417 			// Confirmation dialog for deleting a theme.
   3418 			if ( ! window.confirm( api.settings.l10n.confirmDeleteTheme ) ) {
   3419 				return;
   3420 			}
   3421 
   3422 			wp.updates.maybeRequestFilesystemCredentials( event );
   3423 
   3424 			$( document ).one( 'wp-theme-delete-success', function() {
   3425 				var control = api.control( 'installed_theme_' + theme );
   3426 
   3427 				// Remove theme control.
   3428 				control.container.remove();
   3429 				api.control.remove( control.id );
   3430 
   3431 				// Update installed count.
   3432 				section.loaded = section.loaded - 1;
   3433 				section.updateCount();
   3434 
   3435 				// Rerender any other theme controls as uninstalled.
   3436 				api.control.each( function( control ) {
   3437 					if ( 'theme' === control.params.type && control.params.theme.id === theme ) {
   3438 						control.rerenderAsInstalled( false );
   3439 					}
   3440 				});
   3441 			} );
   3442 
   3443 			wp.updates.deleteTheme( {
   3444 				slug: theme
   3445 			} );
   3446 
   3447 			// Close modal and focus the section.
   3448 			section.closeDetails();
   3449 			section.focus();
   3450 		}
   3451 	});
   3452 
   3453 	api.Control = api.Class.extend(/** @lends wp.customize.Control.prototype */{
   3454 		defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
   3455 
   3456 		/**
   3457 		 * Default params.
   3458 		 *
   3459 		 * @since 4.9.0
   3460 		 * @var {object}
   3461 		 */
   3462 		defaults: {
   3463 			label: '',
   3464 			description: '',
   3465 			active: true,
   3466 			priority: 10
   3467 		},
   3468 
   3469 		/**
   3470 		 * A Customizer Control.
   3471 		 *
   3472 		 * A control provides a UI element that allows a user to modify a Customizer Setting.
   3473 		 *
   3474 		 * @see PHP class WP_Customize_Control.
   3475 		 *
   3476 		 * @constructs wp.customize.Control
   3477 		 * @augments   wp.customize.Class
   3478 		 *
   3479 		 * @borrows wp.customize~focus as this#focus
   3480 		 * @borrows wp.customize~Container#activate as this#activate
   3481 		 * @borrows wp.customize~Container#deactivate as this#deactivate
   3482 		 * @borrows wp.customize~Container#_toggleActive as this#_toggleActive
   3483 		 *
   3484 		 * @param {string} id                       - Unique identifier for the control instance.
   3485 		 * @param {Object} options                  - Options hash for the control instance.
   3486 		 * @param {Object} options.type             - Type of control (e.g. text, radio, dropdown-pages, etc.)
   3487 		 * @param {string} [options.content]        - The HTML content for the control or at least its container. This should normally be left blank and instead supplying a templateId.
   3488 		 * @param {string} [options.templateId]     - Template ID for control's content.
   3489 		 * @param {string} [options.priority=10]    - Order of priority to show the control within the section.
   3490 		 * @param {string} [options.active=true]    - Whether the control is active.
   3491 		 * @param {string} options.section          - The ID of the section the control belongs to.
   3492 		 * @param {mixed}  [options.setting]        - The ID of the main setting or an instance of this setting.
   3493 		 * @param {mixed}  options.settings         - An object with keys (e.g. default) that maps to setting IDs or Setting/Value objects, or an array of setting IDs or Setting/Value objects.
   3494 		 * @param {mixed}  options.settings.default - The ID of the setting the control relates to.
   3495 		 * @param {string} options.settings.data    - @todo Is this used?
   3496 		 * @param {string} options.label            - Label.
   3497 		 * @param {string} options.description      - Description.
   3498 		 * @param {number} [options.instanceNumber] - Order in which this instance was created in relation to other instances.
   3499 		 * @param {Object} [options.params]         - Deprecated wrapper for the above properties.
   3500 		 * @return {void}
   3501 		 */
   3502 		initialize: function( id, options ) {
   3503 			var control = this, deferredSettingIds = [], settings, gatherSettings;
   3504 
   3505 			control.params = _.extend(
   3506 				{},
   3507 				control.defaults,
   3508 				control.params || {}, // In case subclass already defines.
   3509 				options.params || options || {} // The options.params property is deprecated, but it is checked first for back-compat.
   3510 			);
   3511 
   3512 			if ( ! api.Control.instanceCounter ) {
   3513 				api.Control.instanceCounter = 0;
   3514 			}
   3515 			api.Control.instanceCounter++;
   3516 			if ( ! control.params.instanceNumber ) {
   3517 				control.params.instanceNumber = api.Control.instanceCounter;
   3518 			}
   3519 
   3520 			// Look up the type if one was not supplied.
   3521 			if ( ! control.params.type ) {
   3522 				_.find( api.controlConstructor, function( Constructor, type ) {
   3523 					if ( Constructor === control.constructor ) {
   3524 						control.params.type = type;
   3525 						return true;
   3526 					}
   3527 					return false;
   3528 				} );
   3529 			}
   3530 
   3531 			if ( ! control.params.content ) {
   3532 				control.params.content = $( '<li></li>', {
   3533 					id: 'customize-control-' + id.replace( /]/g, '' ).replace( /\[/g, '-' ),
   3534 					'class': 'customize-control customize-control-' + control.params.type
   3535 				} );
   3536 			}
   3537 
   3538 			control.id = id;
   3539 			control.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' ); // Deprecated, likely dead code from time before #28709.
   3540 			if ( control.params.content ) {
   3541 				control.container = $( control.params.content );
   3542 			} else {
   3543 				control.container = $( control.selector ); // Likely dead, per above. See #28709.
   3544 			}
   3545 
   3546 			if ( control.params.templateId ) {
   3547 				control.templateSelector = control.params.templateId;
   3548 			} else {
   3549 				control.templateSelector = 'customize-control-' + control.params.type + '-content';
   3550 			}
   3551 
   3552 			control.deferred = _.extend( control.deferred || {}, {
   3553 				embedded: new $.Deferred()
   3554 			} );
   3555 			control.section = new api.Value();
   3556 			control.priority = new api.Value();
   3557 			control.active = new api.Value();
   3558 			control.activeArgumentsQueue = [];
   3559 			control.notifications = new api.Notifications({
   3560 				alt: control.altNotice
   3561 			});
   3562 
   3563 			control.elements = [];
   3564 
   3565 			control.active.bind( function ( active ) {
   3566 				var args = control.activeArgumentsQueue.shift();
   3567 				args = $.extend( {}, control.defaultActiveArguments, args );
   3568 				control.onChangeActive( active, args );
   3569 			} );
   3570 
   3571 			control.section.set( control.params.section );
   3572 			control.priority.set( isNaN( control.params.priority ) ? 10 : control.params.priority );
   3573 			control.active.set( control.params.active );
   3574 
   3575 			api.utils.bubbleChildValueChanges( control, [ 'section', 'priority', 'active' ] );
   3576 
   3577 			control.settings = {};
   3578 
   3579 			settings = {};
   3580 			if ( control.params.setting ) {
   3581 				settings['default'] = control.params.setting;
   3582 			}
   3583 			_.extend( settings, control.params.settings );
   3584 
   3585 			// Note: Settings can be an array or an object, with values being either setting IDs or Setting (or Value) objects.
   3586 			_.each( settings, function( value, key ) {
   3587 				var setting;
   3588 				if ( _.isObject( value ) && _.isFunction( value.extended ) && value.extended( api.Value ) ) {
   3589 					control.settings[ key ] = value;
   3590 				} else if ( _.isString( value ) ) {
   3591 					setting = api( value );
   3592 					if ( setting ) {
   3593 						control.settings[ key ] = setting;
   3594 					} else {
   3595 						deferredSettingIds.push( value );
   3596 					}
   3597 				}
   3598 			} );
   3599 
   3600 			gatherSettings = function() {
   3601 
   3602 				// Fill-in all resolved settings.
   3603 				_.each( settings, function ( settingId, key ) {
   3604 					if ( ! control.settings[ key ] && _.isString( settingId ) ) {
   3605 						control.settings[ key ] = api( settingId );
   3606 					}
   3607 				} );
   3608 
   3609 				// Make sure settings passed as array gets associated with default.
   3610 				if ( control.settings[0] && ! control.settings['default'] ) {
   3611 					control.settings['default'] = control.settings[0];
   3612 				}
   3613 
   3614 				// Identify the main setting.
   3615 				control.setting = control.settings['default'] || null;
   3616 
   3617 				control.linkElements(); // Link initial elements present in server-rendered content.
   3618 				control.embed();
   3619 			};
   3620 
   3621 			if ( 0 === deferredSettingIds.length ) {
   3622 				gatherSettings();
   3623 			} else {
   3624 				api.apply( api, deferredSettingIds.concat( gatherSettings ) );
   3625 			}
   3626 
   3627 			// After the control is embedded on the page, invoke the "ready" method.
   3628 			control.deferred.embedded.done( function () {
   3629 				control.linkElements(); // Link any additional elements after template is rendered by renderContent().
   3630 				control.setupNotifications();
   3631 				control.ready();
   3632 			});
   3633 		},
   3634 
   3635 		/**
   3636 		 * Link elements between settings and inputs.
   3637 		 *
   3638 		 * @since 4.7.0
   3639 		 * @access public
   3640 		 *
   3641 		 * @return {void}
   3642 		 */
   3643 		linkElements: function () {
   3644 			var control = this, nodes, radios, element;
   3645 
   3646 			nodes = control.container.find( '[data-customize-setting-link], [data-customize-setting-key-link]' );
   3647 			radios = {};
   3648 
   3649 			nodes.each( function () {
   3650 				var node = $( this ), name, setting;
   3651 
   3652 				if ( node.data( 'customizeSettingLinked' ) ) {
   3653 					return;
   3654 				}
   3655 				node.data( 'customizeSettingLinked', true ); // Prevent re-linking element.
   3656 
   3657 				if ( node.is( ':radio' ) ) {
   3658 					name = node.prop( 'name' );
   3659 					if ( radios[name] ) {
   3660 						return;
   3661 					}
   3662 
   3663 					radios[name] = true;
   3664 					node = nodes.filter( '[name="' + name + '"]' );
   3665 				}
   3666 
   3667 				// Let link by default refer to setting ID. If it doesn't exist, fallback to looking up by setting key.
   3668 				if ( node.data( 'customizeSettingLink' ) ) {
   3669 					setting = api( node.data( 'customizeSettingLink' ) );
   3670 				} else if ( node.data( 'customizeSettingKeyLink' ) ) {
   3671 					setting = control.settings[ node.data( 'customizeSettingKeyLink' ) ];
   3672 				}
   3673 
   3674 				if ( setting ) {
   3675 					element = new api.Element( node );
   3676 					control.elements.push( element );
   3677 					element.sync( setting );
   3678 					element.set( setting() );
   3679 				}
   3680 			} );
   3681 		},
   3682 
   3683 		/**
   3684 		 * Embed the control into the page.
   3685 		 */
   3686 		embed: function () {
   3687 			var control = this,
   3688 				inject;
   3689 
   3690 			// Watch for changes to the section state.
   3691 			inject = function ( sectionId ) {
   3692 				var parentContainer;
   3693 				if ( ! sectionId ) { // @todo Allow a control to be embedded without a section, for instance a control embedded in the front end.
   3694 					return;
   3695 				}
   3696 				// Wait for the section to be registered.
   3697 				api.section( sectionId, function ( section ) {
   3698 					// Wait for the section to be ready/initialized.
   3699 					section.deferred.embedded.done( function () {
   3700 						parentContainer = ( section.contentContainer.is( 'ul' ) ) ? section.contentContainer : section.contentContainer.find( 'ul:first' );
   3701 						if ( ! control.container.parent().is( parentContainer ) ) {
   3702 							parentContainer.append( control.container );
   3703 						}
   3704 						control.renderContent();
   3705 						control.deferred.embedded.resolve();
   3706 					});
   3707 				});
   3708 			};
   3709 			control.section.bind( inject );
   3710 			inject( control.section.get() );
   3711 		},
   3712 
   3713 		/**
   3714 		 * Triggered when the control's markup has been injected into the DOM.
   3715 		 *
   3716 		 * @return {void}
   3717 		 */
   3718 		ready: function() {
   3719 			var control = this, newItem;
   3720 			if ( 'dropdown-pages' === control.params.type && control.params.allow_addition ) {
   3721 				newItem = control.container.find( '.new-content-item' );
   3722 				newItem.hide(); // Hide in JS to preserve flex display when showing.
   3723 				control.container.on( 'click', '.add-new-toggle', function( e ) {
   3724 					$( e.currentTarget ).slideUp( 180 );
   3725 					newItem.slideDown( 180 );
   3726 					newItem.find( '.create-item-input' ).focus();
   3727 				});
   3728 				control.container.on( 'click', '.add-content', function() {
   3729 					control.addNewPage();
   3730 				});
   3731 				control.container.on( 'keydown', '.create-item-input', function( e ) {
   3732 					if ( 13 === e.which ) { // Enter.
   3733 						control.addNewPage();
   3734 					}
   3735 				});
   3736 			}
   3737 		},
   3738 
   3739 		/**
   3740 		 * Get the element inside of a control's container that contains the validation error message.
   3741 		 *
   3742 		 * Control subclasses may override this to return the proper container to render notifications into.
   3743 		 * Injects the notification container for existing controls that lack the necessary container,
   3744 		 * including special handling for nav menu items and widgets.
   3745 		 *
   3746 		 * @since 4.6.0
   3747 		 * @return {jQuery} Setting validation message element.
   3748 		 */
   3749 		getNotificationsContainerElement: function() {
   3750 			var control = this, controlTitle, notificationsContainer;
   3751 
   3752 			notificationsContainer = control.container.find( '.customize-control-notifications-container:first' );
   3753 			if ( notificationsContainer.length ) {
   3754 				return notificationsContainer;
   3755 			}
   3756 
   3757 			notificationsContainer = $( '<div class="customize-control-notifications-container"></div>' );
   3758 
   3759 			if ( control.container.hasClass( 'customize-control-nav_menu_item' ) ) {
   3760 				control.container.find( '.menu-item-settings:first' ).prepend( notificationsContainer );
   3761 			} else if ( control.container.hasClass( 'customize-control-widget_form' ) ) {
   3762 				control.container.find( '.widget-inside:first' ).prepend( notificationsContainer );
   3763 			} else {
   3764 				controlTitle = control.container.find( '.customize-control-title' );
   3765 				if ( controlTitle.length ) {
   3766 					controlTitle.after( notificationsContainer );
   3767 				} else {
   3768 					control.container.prepend( notificationsContainer );
   3769 				}
   3770 			}
   3771 			return notificationsContainer;
   3772 		},
   3773 
   3774 		/**
   3775 		 * Set up notifications.
   3776 		 *
   3777 		 * @since 4.9.0
   3778 		 * @return {void}
   3779 		 */
   3780 		setupNotifications: function() {
   3781 			var control = this, renderNotificationsIfVisible, onSectionAssigned;
   3782 
   3783 			// Add setting notifications to the control notification.
   3784 			_.each( control.settings, function( setting ) {
   3785 				if ( ! setting.notifications ) {
   3786 					return;
   3787 				}
   3788 				setting.notifications.bind( 'add', function( settingNotification ) {
   3789 					var params = _.extend(
   3790 						{},
   3791 						settingNotification,
   3792 						{
   3793 							setting: setting.id
   3794 						}
   3795 					);
   3796 					control.notifications.add( new api.Notification( setting.id + ':' + settingNotification.code, params ) );
   3797 				} );
   3798 				setting.notifications.bind( 'remove', function( settingNotification ) {
   3799 					control.notifications.remove( setting.id + ':' + settingNotification.code );
   3800 				} );
   3801 			} );
   3802 
   3803 			renderNotificationsIfVisible = function() {
   3804 				var sectionId = control.section();
   3805 				if ( ! sectionId || ( api.section.has( sectionId ) && api.section( sectionId ).expanded() ) ) {
   3806 					control.notifications.render();
   3807 				}
   3808 			};
   3809 
   3810 			control.notifications.bind( 'rendered', function() {
   3811 				var notifications = control.notifications.get();
   3812 				control.container.toggleClass( 'has-notifications', 0 !== notifications.length );
   3813 				control.container.toggleClass( 'has-error', 0 !== _.where( notifications, { type: 'error' } ).length );
   3814 			} );
   3815 
   3816 			onSectionAssigned = function( newSectionId, oldSectionId ) {
   3817 				if ( oldSectionId && api.section.has( oldSectionId ) ) {
   3818 					api.section( oldSectionId ).expanded.unbind( renderNotificationsIfVisible );
   3819 				}
   3820 				if ( newSectionId ) {
   3821 					api.section( newSectionId, function( section ) {
   3822 						section.expanded.bind( renderNotificationsIfVisible );
   3823 						renderNotificationsIfVisible();
   3824 					});
   3825 				}
   3826 			};
   3827 
   3828 			control.section.bind( onSectionAssigned );
   3829 			onSectionAssigned( control.section.get() );
   3830 			control.notifications.bind( 'change', _.debounce( renderNotificationsIfVisible ) );
   3831 		},
   3832 
   3833 		/**
   3834 		 * Render notifications.
   3835 		 *
   3836 		 * Renders the `control.notifications` into the control's container.
   3837 		 * Control subclasses may override this method to do their own handling
   3838 		 * of rendering notifications.
   3839 		 *
   3840 		 * @deprecated in favor of `control.notifications.render()`
   3841 		 * @since 4.6.0
   3842 		 * @this {wp.customize.Control}
   3843 		 */
   3844 		renderNotifications: function() {
   3845 			var control = this, container, notifications, hasError = false;
   3846 
   3847 			if ( 'undefined' !== typeof console && console.warn ) {
   3848 				console.warn( '[DEPRECATED] wp.customize.Control.prototype.renderNotifications() is deprecated in favor of instantating a wp.customize.Notifications and calling its render() method.' );
   3849 			}
   3850 
   3851 			container = control.getNotificationsContainerElement();
   3852 			if ( ! container || ! container.length ) {
   3853 				return;
   3854 			}
   3855 			notifications = [];
   3856 			control.notifications.each( function( notification ) {
   3857 				notifications.push( notification );
   3858 				if ( 'error' === notification.type ) {
   3859 					hasError = true;
   3860 				}
   3861 			} );
   3862 
   3863 			if ( 0 === notifications.length ) {
   3864 				container.stop().slideUp( 'fast' );
   3865 			} else {
   3866 				container.stop().slideDown( 'fast', null, function() {
   3867 					$( this ).css( 'height', 'auto' );
   3868 				} );
   3869 			}
   3870 
   3871 			if ( ! control.notificationsTemplate ) {
   3872 				control.notificationsTemplate = wp.template( 'customize-control-notifications' );
   3873 			}
   3874 
   3875 			control.container.toggleClass( 'has-notifications', 0 !== notifications.length );
   3876 			control.container.toggleClass( 'has-error', hasError );
   3877 			container.empty().append(
   3878 				control.notificationsTemplate( { notifications: notifications, altNotice: Boolean( control.altNotice ) } ).trim()
   3879 			);
   3880 		},
   3881 
   3882 		/**
   3883 		 * Normal controls do not expand, so just expand its parent
   3884 		 *
   3885 		 * @param {Object} [params]
   3886 		 */
   3887 		expand: function ( params ) {
   3888 			api.section( this.section() ).expand( params );
   3889 		},
   3890 
   3891 		/*
   3892 		 * Documented using @borrows in the constructor.
   3893 		 */
   3894 		focus: focus,
   3895 
   3896 		/**
   3897 		 * Update UI in response to a change in the control's active state.
   3898 		 * This does not change the active state, it merely handles the behavior
   3899 		 * for when it does change.
   3900 		 *
   3901 		 * @since 4.1.0
   3902 		 *
   3903 		 * @param {boolean}  active
   3904 		 * @param {Object}   args
   3905 		 * @param {number}   args.duration
   3906 		 * @param {Function} args.completeCallback
   3907 		 */
   3908 		onChangeActive: function ( active, args ) {
   3909 			if ( args.unchanged ) {
   3910 				if ( args.completeCallback ) {
   3911 					args.completeCallback();
   3912 				}
   3913 				return;
   3914 			}
   3915 
   3916 			if ( ! $.contains( document, this.container[0] ) ) {
   3917 				// jQuery.fn.slideUp is not hiding an element if it is not in the DOM.
   3918 				this.container.toggle( active );
   3919 				if ( args.completeCallback ) {
   3920 					args.completeCallback();
   3921 				}
   3922 			} else if ( active ) {
   3923 				this.container.slideDown( args.duration, args.completeCallback );
   3924 			} else {
   3925 				this.container.slideUp( args.duration, args.completeCallback );
   3926 			}
   3927 		},
   3928 
   3929 		/**
   3930 		 * @deprecated 4.1.0 Use this.onChangeActive() instead.
   3931 		 */
   3932 		toggle: function ( active ) {
   3933 			return this.onChangeActive( active, this.defaultActiveArguments );
   3934 		},
   3935 
   3936 		/*
   3937 		 * Documented using @borrows in the constructor
   3938 		 */
   3939 		activate: Container.prototype.activate,
   3940 
   3941 		/*
   3942 		 * Documented using @borrows in the constructor
   3943 		 */
   3944 		deactivate: Container.prototype.deactivate,
   3945 
   3946 		/*
   3947 		 * Documented using @borrows in the constructor
   3948 		 */
   3949 		_toggleActive: Container.prototype._toggleActive,
   3950 
   3951 		// @todo This function appears to be dead code and can be removed.
   3952 		dropdownInit: function() {
   3953 			var control      = this,
   3954 				statuses     = this.container.find('.dropdown-status'),
   3955 				params       = this.params,
   3956 				toggleFreeze = false,
   3957 				update       = function( to ) {
   3958 					if ( 'string' === typeof to && params.statuses && params.statuses[ to ] ) {
   3959 						statuses.html( params.statuses[ to ] ).show();
   3960 					} else {
   3961 						statuses.hide();
   3962 					}
   3963 				};
   3964 
   3965 			// Support the .dropdown class to open/close complex elements.
   3966 			this.container.on( 'click keydown', '.dropdown', function( event ) {
   3967 				if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
   3968 					return;
   3969 				}
   3970 
   3971 				event.preventDefault();
   3972 
   3973 				if ( ! toggleFreeze ) {
   3974 					control.container.toggleClass( 'open' );
   3975 				}
   3976 
   3977 				if ( control.container.hasClass( 'open' ) ) {
   3978 					control.container.parent().parent().find( 'li.library-selected' ).focus();
   3979 				}
   3980 
   3981 				// Don't want to fire focus and click at same time.
   3982 				toggleFreeze = true;
   3983 				setTimeout(function () {
   3984 					toggleFreeze = false;
   3985 				}, 400);
   3986 			});
   3987 
   3988 			this.setting.bind( update );
   3989 			update( this.setting() );
   3990 		},
   3991 
   3992 		/**
   3993 		 * Render the control from its JS template, if it exists.
   3994 		 *
   3995 		 * The control's container must already exist in the DOM.
   3996 		 *
   3997 		 * @since 4.1.0
   3998 		 */
   3999 		renderContent: function () {
   4000 			var control = this, template, standardTypes, templateId, sectionId;
   4001 
   4002 			standardTypes = [
   4003 				'button',
   4004 				'checkbox',
   4005 				'date',
   4006 				'datetime-local',
   4007 				'email',
   4008 				'month',
   4009 				'number',
   4010 				'password',
   4011 				'radio',
   4012 				'range',
   4013 				'search',
   4014 				'select',
   4015 				'tel',
   4016 				'time',
   4017 				'text',
   4018 				'textarea',
   4019 				'week',
   4020 				'url'
   4021 			];
   4022 
   4023 			templateId = control.templateSelector;
   4024 
   4025 			// Use default content template when a standard HTML type is used,
   4026 			// there isn't a more specific template existing, and the control container is empty.
   4027 			if ( templateId === 'customize-control-' + control.params.type + '-content' &&
   4028 				_.contains( standardTypes, control.params.type ) &&
   4029 				! document.getElementById( 'tmpl-' + templateId ) &&
   4030 				0 === control.container.children().length )
   4031 			{
   4032 				templateId = 'customize-control-default-content';
   4033 			}
   4034 
   4035 			// Replace the container element's content with the control.
   4036 			if ( document.getElementById( 'tmpl-' + templateId ) ) {
   4037 				template = wp.template( templateId );
   4038 				if ( template && control.container ) {
   4039 					control.container.html( template( control.params ) );
   4040 				}
   4041 			}
   4042 
   4043 			// Re-render notifications after content has been re-rendered.
   4044 			control.notifications.container = control.getNotificationsContainerElement();
   4045 			sectionId = control.section();
   4046 			if ( ! sectionId || ( api.section.has( sectionId ) && api.section( sectionId ).expanded() ) ) {
   4047 				control.notifications.render();
   4048 			}
   4049 		},
   4050 
   4051 		/**
   4052 		 * Add a new page to a dropdown-pages control reusing menus code for this.
   4053 		 *
   4054 		 * @since 4.7.0
   4055 		 * @access private
   4056 		 *
   4057 		 * @return {void}
   4058 		 */
   4059 		addNewPage: function () {
   4060 			var control = this, promise, toggle, container, input, title, select;
   4061 
   4062 			if ( 'dropdown-pages' !== control.params.type || ! control.params.allow_addition || ! api.Menus ) {
   4063 				return;
   4064 			}
   4065 
   4066 			toggle = control.container.find( '.add-new-toggle' );
   4067 			container = control.container.find( '.new-content-item' );
   4068 			input = control.container.find( '.create-item-input' );
   4069 			title = input.val();
   4070 			select = control.container.find( 'select' );
   4071 
   4072 			if ( ! title ) {
   4073 				input.addClass( 'invalid' );
   4074 				return;
   4075 			}
   4076 
   4077 			input.removeClass( 'invalid' );
   4078 			input.attr( 'disabled', 'disabled' );
   4079 
   4080 			// The menus functions add the page, publish when appropriate,
   4081 			// and also add the new page to the dropdown-pages controls.
   4082 			promise = api.Menus.insertAutoDraftPost( {
   4083 				post_title: title,
   4084 				post_type: 'page'
   4085 			} );
   4086 			promise.done( function( data ) {
   4087 				var availableItem, $content, itemTemplate;
   4088 
   4089 				// Prepare the new page as an available menu item.
   4090 				// See api.Menus.submitNew().
   4091 				availableItem = new api.Menus.AvailableItemModel( {
   4092 					'id': 'post-' + data.post_id, // Used for available menu item Backbone models.
   4093 					'title': title,
   4094 					'type': 'post_type',
   4095 					'type_label': api.Menus.data.l10n.page_label,
   4096 					'object': 'page',
   4097 					'object_id': data.post_id,
   4098 					'url': data.url
   4099 				} );
   4100 
   4101 				// Add the new item to the list of available menu items.
   4102 				api.Menus.availableMenuItemsPanel.collection.add( availableItem );
   4103 				$content = $( '#available-menu-items-post_type-page' ).find( '.available-menu-items-list' );
   4104 				itemTemplate = wp.template( 'available-menu-item' );
   4105 				$content.prepend( itemTemplate( availableItem.attributes ) );
   4106 
   4107 				// Focus the select control.
   4108 				select.focus();
   4109 				control.setting.set( String( data.post_id ) ); // Triggers a preview refresh and updates the setting.
   4110 
   4111 				// Reset the create page form.
   4112 				container.slideUp( 180 );
   4113 				toggle.slideDown( 180 );
   4114 			} );
   4115 			promise.always( function() {
   4116 				input.val( '' ).removeAttr( 'disabled' );
   4117 			} );
   4118 		}
   4119 	});
   4120 
   4121 	/**
   4122 	 * A colorpicker control.
   4123 	 *
   4124 	 * @class    wp.customize.ColorControl
   4125 	 * @augments wp.customize.Control
   4126 	 */
   4127 	api.ColorControl = api.Control.extend(/** @lends wp.customize.ColorControl.prototype */{
   4128 		ready: function() {
   4129 			var control = this,
   4130 				isHueSlider = this.params.mode === 'hue',
   4131 				updating = false,
   4132 				picker;
   4133 
   4134 			if ( isHueSlider ) {
   4135 				picker = this.container.find( '.color-picker-hue' );
   4136 				picker.val( control.setting() ).wpColorPicker({
   4137 					change: function( event, ui ) {
   4138 						updating = true;
   4139 						control.setting( ui.color.h() );
   4140 						updating = false;
   4141 					}
   4142 				});
   4143 			} else {
   4144 				picker = this.container.find( '.color-picker-hex' );
   4145 				picker.val( control.setting() ).wpColorPicker({
   4146 					change: function() {
   4147 						updating = true;
   4148 						control.setting.set( picker.wpColorPicker( 'color' ) );
   4149 						updating = false;
   4150 					},
   4151 					clear: function() {
   4152 						updating = true;
   4153 						control.setting.set( '' );
   4154 						updating = false;
   4155 					}
   4156 				});
   4157 			}
   4158 
   4159 			control.setting.bind( function ( value ) {
   4160 				// Bail if the update came from the control itself.
   4161 				if ( updating ) {
   4162 					return;
   4163 				}
   4164 				picker.val( value );
   4165 				picker.wpColorPicker( 'color', value );
   4166 			} );
   4167 
   4168 			// Collapse color picker when hitting Esc instead of collapsing the current section.
   4169 			control.container.on( 'keydown', function( event ) {
   4170 				var pickerContainer;
   4171 				if ( 27 !== event.which ) { // Esc.
   4172 					return;
   4173 				}
   4174 				pickerContainer = control.container.find( '.wp-picker-container' );
   4175 				if ( pickerContainer.hasClass( 'wp-picker-active' ) ) {
   4176 					picker.wpColorPicker( 'close' );
   4177 					control.container.find( '.wp-color-result' ).focus();
   4178 					event.stopPropagation(); // Prevent section from being collapsed.
   4179 				}
   4180 			} );
   4181 		}
   4182 	});
   4183 
   4184 	/**
   4185 	 * A control that implements the media modal.
   4186 	 *
   4187 	 * @class    wp.customize.MediaControl
   4188 	 * @augments wp.customize.Control
   4189 	 */
   4190 	api.MediaControl = api.Control.extend(/** @lends wp.customize.MediaControl.prototype */{
   4191 
   4192 		/**
   4193 		 * When the control's DOM structure is ready,
   4194 		 * set up internal event bindings.
   4195 		 */
   4196 		ready: function() {
   4197 			var control = this;
   4198 			// Shortcut so that we don't have to use _.bind every time we add a callback.
   4199 			_.bindAll( control, 'restoreDefault', 'removeFile', 'openFrame', 'select', 'pausePlayer' );
   4200 
   4201 			// Bind events, with delegation to facilitate re-rendering.
   4202 			control.container.on( 'click keydown', '.upload-button', control.openFrame );
   4203 			control.container.on( 'click keydown', '.upload-button', control.pausePlayer );
   4204 			control.container.on( 'click keydown', '.thumbnail-image img', control.openFrame );
   4205 			control.container.on( 'click keydown', '.default-button', control.restoreDefault );
   4206 			control.container.on( 'click keydown', '.remove-button', control.pausePlayer );
   4207 			control.container.on( 'click keydown', '.remove-button', control.removeFile );
   4208 			control.container.on( 'click keydown', '.remove-button', control.cleanupPlayer );
   4209 
   4210 			// Resize the player controls when it becomes visible (ie when section is expanded).
   4211 			api.section( control.section() ).container
   4212 				.on( 'expanded', function() {
   4213 					if ( control.player ) {
   4214 						control.player.setControlsSize();
   4215 					}
   4216 				})
   4217 				.on( 'collapsed', function() {
   4218 					control.pausePlayer();
   4219 				});
   4220 
   4221 			/**
   4222 			 * Set attachment data and render content.
   4223 			 *
   4224 			 * Note that BackgroundImage.prototype.ready applies this ready method
   4225 			 * to itself. Since BackgroundImage is an UploadControl, the value
   4226 			 * is the attachment URL instead of the attachment ID. In this case
   4227 			 * we skip fetching the attachment data because we have no ID available,
   4228 			 * and it is the responsibility of the UploadControl to set the control's
   4229 			 * attachmentData before calling the renderContent method.
   4230 			 *
   4231 			 * @param {number|string} value Attachment
   4232 			 */
   4233 			function setAttachmentDataAndRenderContent( value ) {
   4234 				var hasAttachmentData = $.Deferred();
   4235 
   4236 				if ( control.extended( api.UploadControl ) ) {
   4237 					hasAttachmentData.resolve();
   4238 				} else {
   4239 					value = parseInt( value, 10 );
   4240 					if ( _.isNaN( value ) || value <= 0 ) {
   4241 						delete control.params.attachment;
   4242 						hasAttachmentData.resolve();
   4243 					} else if ( control.params.attachment && control.params.attachment.id === value ) {
   4244 						hasAttachmentData.resolve();
   4245 					}
   4246 				}
   4247 
   4248 				// Fetch the attachment data.
   4249 				if ( 'pending' === hasAttachmentData.state() ) {
   4250 					wp.media.attachment( value ).fetch().done( function() {
   4251 						control.params.attachment = this.attributes;
   4252 						hasAttachmentData.resolve();
   4253 
   4254 						// Send attachment information to the preview for possible use in `postMessage` transport.
   4255 						wp.customize.previewer.send( control.setting.id + '-attachment-data', this.attributes );
   4256 					} );
   4257 				}
   4258 
   4259 				hasAttachmentData.done( function() {
   4260 					control.renderContent();
   4261 				} );
   4262 			}
   4263 
   4264 			// Ensure attachment data is initially set (for dynamically-instantiated controls).
   4265 			setAttachmentDataAndRenderContent( control.setting() );
   4266 
   4267 			// Update the attachment data and re-render the control when the setting changes.
   4268 			control.setting.bind( setAttachmentDataAndRenderContent );
   4269 		},
   4270 
   4271 		pausePlayer: function () {
   4272 			this.player && this.player.pause();
   4273 		},
   4274 
   4275 		cleanupPlayer: function () {
   4276 			this.player && wp.media.mixin.removePlayer( this.player );
   4277 		},
   4278 
   4279 		/**
   4280 		 * Open the media modal.
   4281 		 */
   4282 		openFrame: function( event ) {
   4283 			if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
   4284 				return;
   4285 			}
   4286 
   4287 			event.preventDefault();
   4288 
   4289 			if ( ! this.frame ) {
   4290 				this.initFrame();
   4291 			}
   4292 
   4293 			this.frame.open();
   4294 		},
   4295 
   4296 		/**
   4297 		 * Create a media modal select frame, and store it so the instance can be reused when needed.
   4298 		 */
   4299 		initFrame: function() {
   4300 			this.frame = wp.media({
   4301 				button: {
   4302 					text: this.params.button_labels.frame_button
   4303 				},
   4304 				states: [
   4305 					new wp.media.controller.Library({
   4306 						title:     this.params.button_labels.frame_title,
   4307 						library:   wp.media.query({ type: this.params.mime_type }),
   4308 						multiple:  false,
   4309 						date:      false
   4310 					})
   4311 				]
   4312 			});
   4313 
   4314 			// When a file is selected, run a callback.
   4315 			this.frame.on( 'select', this.select );
   4316 		},
   4317 
   4318 		/**
   4319 		 * Callback handler for when an attachment is selected in the media modal.
   4320 		 * Gets the selected image information, and sets it within the control.
   4321 		 */
   4322 		select: function() {
   4323 			// Get the attachment from the modal frame.
   4324 			var node,
   4325 				attachment = this.frame.state().get( 'selection' ).first().toJSON(),
   4326 				mejsSettings = window._wpmejsSettings || {};
   4327 
   4328 			this.params.attachment = attachment;
   4329 
   4330 			// Set the Customizer setting; the callback takes care of rendering.
   4331 			this.setting( attachment.id );
   4332 			node = this.container.find( 'audio, video' ).get(0);
   4333 
   4334 			// Initialize audio/video previews.
   4335 			if ( node ) {
   4336 				this.player = new MediaElementPlayer( node, mejsSettings );
   4337 			} else {
   4338 				this.cleanupPlayer();
   4339 			}
   4340 		},
   4341 
   4342 		/**
   4343 		 * Reset the setting to the default value.
   4344 		 */
   4345 		restoreDefault: function( event ) {
   4346 			if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
   4347 				return;
   4348 			}
   4349 			event.preventDefault();
   4350 
   4351 			this.params.attachment = this.params.defaultAttachment;
   4352 			this.setting( this.params.defaultAttachment.url );
   4353 		},
   4354 
   4355 		/**
   4356 		 * Called when the "Remove" link is clicked. Empties the setting.
   4357 		 *
   4358 		 * @param {Object} event jQuery Event object
   4359 		 */
   4360 		removeFile: function( event ) {
   4361 			if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
   4362 				return;
   4363 			}
   4364 			event.preventDefault();
   4365 
   4366 			this.params.attachment = {};
   4367 			this.setting( '' );
   4368 			this.renderContent(); // Not bound to setting change when emptying.
   4369 		}
   4370 	});
   4371 
   4372 	/**
   4373 	 * An upload control, which utilizes the media modal.
   4374 	 *
   4375 	 * @class    wp.customize.UploadControl
   4376 	 * @augments wp.customize.MediaControl
   4377 	 */
   4378 	api.UploadControl = api.MediaControl.extend(/** @lends wp.customize.UploadControl.prototype */{
   4379 
   4380 		/**
   4381 		 * Callback handler for when an attachment is selected in the media modal.
   4382 		 * Gets the selected image information, and sets it within the control.
   4383 		 */
   4384 		select: function() {
   4385 			// Get the attachment from the modal frame.
   4386 			var node,
   4387 				attachment = this.frame.state().get( 'selection' ).first().toJSON(),
   4388 				mejsSettings = window._wpmejsSettings || {};
   4389 
   4390 			this.params.attachment = attachment;
   4391 
   4392 			// Set the Customizer setting; the callback takes care of rendering.
   4393 			this.setting( attachment.url );
   4394 			node = this.container.find( 'audio, video' ).get(0);
   4395 
   4396 			// Initialize audio/video previews.
   4397 			if ( node ) {
   4398 				this.player = new MediaElementPlayer( node, mejsSettings );
   4399 			} else {
   4400 				this.cleanupPlayer();
   4401 			}
   4402 		},
   4403 
   4404 		// @deprecated
   4405 		success: function() {},
   4406 
   4407 		// @deprecated
   4408 		removerVisibility: function() {}
   4409 	});
   4410 
   4411 	/**
   4412 	 * A control for uploading images.
   4413 	 *
   4414 	 * This control no longer needs to do anything more
   4415 	 * than what the upload control does in JS.
   4416 	 *
   4417 	 * @class    wp.customize.ImageControl
   4418 	 * @augments wp.customize.UploadControl
   4419 	 */
   4420 	api.ImageControl = api.UploadControl.extend(/** @lends wp.customize.ImageControl.prototype */{
   4421 		// @deprecated
   4422 		thumbnailSrc: function() {}
   4423 	});
   4424 
   4425 	/**
   4426 	 * A control for uploading background images.
   4427 	 *
   4428 	 * @class    wp.customize.BackgroundControl
   4429 	 * @augments wp.customize.UploadControl
   4430 	 */
   4431 	api.BackgroundControl = api.UploadControl.extend(/** @lends wp.customize.BackgroundControl.prototype */{
   4432 
   4433 		/**
   4434 		 * When the control's DOM structure is ready,
   4435 		 * set up internal event bindings.
   4436 		 */
   4437 		ready: function() {
   4438 			api.UploadControl.prototype.ready.apply( this, arguments );
   4439 		},
   4440 
   4441 		/**
   4442 		 * Callback handler for when an attachment is selected in the media modal.
   4443 		 * Does an additional Ajax request for setting the background context.
   4444 		 */
   4445 		select: function() {
   4446 			api.UploadControl.prototype.select.apply( this, arguments );
   4447 
   4448 			wp.ajax.post( 'custom-background-add', {
   4449 				nonce: _wpCustomizeBackground.nonces.add,
   4450 				wp_customize: 'on',
   4451 				customize_theme: api.settings.theme.stylesheet,
   4452 				attachment_id: this.params.attachment.id
   4453 			} );
   4454 		}
   4455 	});
   4456 
   4457 	/**
   4458 	 * A control for positioning a background image.
   4459 	 *
   4460 	 * @since 4.7.0
   4461 	 *
   4462 	 * @class    wp.customize.BackgroundPositionControl
   4463 	 * @augments wp.customize.Control
   4464 	 */
   4465 	api.BackgroundPositionControl = api.Control.extend(/** @lends wp.customize.BackgroundPositionControl.prototype */{
   4466 
   4467 		/**
   4468 		 * Set up control UI once embedded in DOM and settings are created.
   4469 		 *
   4470 		 * @since 4.7.0
   4471 		 * @access public
   4472 		 */
   4473 		ready: function() {
   4474 			var control = this, updateRadios;
   4475 
   4476 			control.container.on( 'change', 'input[name="background-position"]', function() {
   4477 				var position = $( this ).val().split( ' ' );
   4478 				control.settings.x( position[0] );
   4479 				control.settings.y( position[1] );
   4480 			} );
   4481 
   4482 			updateRadios = _.debounce( function() {
   4483 				var x, y, radioInput, inputValue;
   4484 				x = control.settings.x.get();
   4485 				y = control.settings.y.get();
   4486 				inputValue = String( x ) + ' ' + String( y );
   4487 				radioInput = control.container.find( 'input[name="background-position"][value="' + inputValue + '"]' );
   4488 				radioInput.trigger( 'click' );
   4489 			} );
   4490 			control.settings.x.bind( updateRadios );
   4491 			control.settings.y.bind( updateRadios );
   4492 
   4493 			updateRadios(); // Set initial UI.
   4494 		}
   4495 	} );
   4496 
   4497 	/**
   4498 	 * A control for selecting and cropping an image.
   4499 	 *
   4500 	 * @class    wp.customize.CroppedImageControl
   4501 	 * @augments wp.customize.MediaControl
   4502 	 */
   4503 	api.CroppedImageControl = api.MediaControl.extend(/** @lends wp.customize.CroppedImageControl.prototype */{
   4504 
   4505 		/**
   4506 		 * Open the media modal to the library state.
   4507 		 */
   4508 		openFrame: function( event ) {
   4509 			if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
   4510 				return;
   4511 			}
   4512 
   4513 			this.initFrame();
   4514 			this.frame.setState( 'library' ).open();
   4515 		},
   4516 
   4517 		/**
   4518 		 * Create a media modal select frame, and store it so the instance can be reused when needed.
   4519 		 */
   4520 		initFrame: function() {
   4521 			var l10n = _wpMediaViewsL10n;
   4522 
   4523 			this.frame = wp.media({
   4524 				button: {
   4525 					text: l10n.select,
   4526 					close: false
   4527 				},
   4528 				states: [
   4529 					new wp.media.controller.Library({
   4530 						title: this.params.button_labels.frame_title,
   4531 						library: wp.media.query({ type: 'image' }),
   4532 						multiple: false,
   4533 						date: false,
   4534 						priority: 20,
   4535 						suggestedWidth: this.params.width,
   4536 						suggestedHeight: this.params.height
   4537 					}),
   4538 					new wp.media.controller.CustomizeImageCropper({
   4539 						imgSelectOptions: this.calculateImageSelectOptions,
   4540 						control: this
   4541 					})
   4542 				]
   4543 			});
   4544 
   4545 			this.frame.on( 'select', this.onSelect, this );
   4546 			this.frame.on( 'cropped', this.onCropped, this );
   4547 			this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
   4548 		},
   4549 
   4550 		/**
   4551 		 * After an image is selected in the media modal, switch to the cropper
   4552 		 * state if the image isn't the right size.
   4553 		 */
   4554 		onSelect: function() {
   4555 			var attachment = this.frame.state().get( 'selection' ).first().toJSON();
   4556 
   4557 			if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
   4558 				this.setImageFromAttachment( attachment );
   4559 				this.frame.close();
   4560 			} else {
   4561 				this.frame.setState( 'cropper' );
   4562 			}
   4563 		},
   4564 
   4565 		/**
   4566 		 * After the image has been cropped, apply the cropped image data to the setting.
   4567 		 *
   4568 		 * @param {Object} croppedImage Cropped attachment data.
   4569 		 */
   4570 		onCropped: function( croppedImage ) {
   4571 			this.setImageFromAttachment( croppedImage );
   4572 		},
   4573 
   4574 		/**
   4575 		 * Returns a set of options, computed from the attached image data and
   4576 		 * control-specific data, to be fed to the imgAreaSelect plugin in
   4577 		 * wp.media.view.Cropper.
   4578 		 *
   4579 		 * @param {wp.media.model.Attachment} attachment
   4580 		 * @param {wp.media.controller.Cropper} controller
   4581 		 * @return {Object} Options
   4582 		 */
   4583 		calculateImageSelectOptions: function( attachment, controller ) {
   4584 			var control    = controller.get( 'control' ),
   4585 				flexWidth  = !! parseInt( control.params.flex_width, 10 ),
   4586 				flexHeight = !! parseInt( control.params.flex_height, 10 ),
   4587 				realWidth  = attachment.get( 'width' ),
   4588 				realHeight = attachment.get( 'height' ),
   4589 				xInit = parseInt( control.params.width, 10 ),
   4590 				yInit = parseInt( control.params.height, 10 ),
   4591 				ratio = xInit / yInit,
   4592 				xImg  = xInit,
   4593 				yImg  = yInit,
   4594 				x1, y1, imgSelectOptions;
   4595 
   4596 			controller.set( 'canSkipCrop', ! control.mustBeCropped( flexWidth, flexHeight, xInit, yInit, realWidth, realHeight ) );
   4597 
   4598 			if ( realWidth / realHeight > ratio ) {
   4599 				yInit = realHeight;
   4600 				xInit = yInit * ratio;
   4601 			} else {
   4602 				xInit = realWidth;
   4603 				yInit = xInit / ratio;
   4604 			}
   4605 
   4606 			x1 = ( realWidth - xInit ) / 2;
   4607 			y1 = ( realHeight - yInit ) / 2;
   4608 
   4609 			imgSelectOptions = {
   4610 				handles: true,
   4611 				keys: true,
   4612 				instance: true,
   4613 				persistent: true,
   4614 				imageWidth: realWidth,
   4615 				imageHeight: realHeight,
   4616 				minWidth: xImg > xInit ? xInit : xImg,
   4617 				minHeight: yImg > yInit ? yInit : yImg,
   4618 				x1: x1,
   4619 				y1: y1,
   4620 				x2: xInit + x1,
   4621 				y2: yInit + y1
   4622 			};
   4623 
   4624 			if ( flexHeight === false && flexWidth === false ) {
   4625 				imgSelectOptions.aspectRatio = xInit + ':' + yInit;
   4626 			}
   4627 
   4628 			if ( true === flexHeight ) {
   4629 				delete imgSelectOptions.minHeight;
   4630 				imgSelectOptions.maxWidth = realWidth;
   4631 			}
   4632 
   4633 			if ( true === flexWidth ) {
   4634 				delete imgSelectOptions.minWidth;
   4635 				imgSelectOptions.maxHeight = realHeight;
   4636 			}
   4637 
   4638 			return imgSelectOptions;
   4639 		},
   4640 
   4641 		/**
   4642 		 * Return whether the image must be cropped, based on required dimensions.
   4643 		 *
   4644 		 * @param {boolean} flexW
   4645 		 * @param {boolean} flexH
   4646 		 * @param {number}  dstW
   4647 		 * @param {number}  dstH
   4648 		 * @param {number}  imgW
   4649 		 * @param {number}  imgH
   4650 		 * @return {boolean}
   4651 		 */
   4652 		mustBeCropped: function( flexW, flexH, dstW, dstH, imgW, imgH ) {
   4653 			if ( true === flexW && true === flexH ) {
   4654 				return false;
   4655 			}
   4656 
   4657 			if ( true === flexW && dstH === imgH ) {
   4658 				return false;
   4659 			}
   4660 
   4661 			if ( true === flexH && dstW === imgW ) {
   4662 				return false;
   4663 			}
   4664 
   4665 			if ( dstW === imgW && dstH === imgH ) {
   4666 				return false;
   4667 			}
   4668 
   4669 			if ( imgW <= dstW ) {
   4670 				return false;
   4671 			}
   4672 
   4673 			return true;
   4674 		},
   4675 
   4676 		/**
   4677 		 * If cropping was skipped, apply the image data directly to the setting.
   4678 		 */
   4679 		onSkippedCrop: function() {
   4680 			var attachment = this.frame.state().get( 'selection' ).first().toJSON();
   4681 			this.setImageFromAttachment( attachment );
   4682 		},
   4683 
   4684 		/**
   4685 		 * Updates the setting and re-renders the control UI.
   4686 		 *
   4687 		 * @param {Object} attachment
   4688 		 */
   4689 		setImageFromAttachment: function( attachment ) {
   4690 			this.params.attachment = attachment;
   4691 
   4692 			// Set the Customizer setting; the callback takes care of rendering.
   4693 			this.setting( attachment.id );
   4694 		}
   4695 	});
   4696 
   4697 	/**
   4698 	 * A control for selecting and cropping Site Icons.
   4699 	 *
   4700 	 * @class    wp.customize.SiteIconControl
   4701 	 * @augments wp.customize.CroppedImageControl
   4702 	 */
   4703 	api.SiteIconControl = api.CroppedImageControl.extend(/** @lends wp.customize.SiteIconControl.prototype */{
   4704 
   4705 		/**
   4706 		 * Create a media modal select frame, and store it so the instance can be reused when needed.
   4707 		 */
   4708 		initFrame: function() {
   4709 			var l10n = _wpMediaViewsL10n;
   4710 
   4711 			this.frame = wp.media({
   4712 				button: {
   4713 					text: l10n.select,
   4714 					close: false
   4715 				},
   4716 				states: [
   4717 					new wp.media.controller.Library({
   4718 						title: this.params.button_labels.frame_title,
   4719 						library: wp.media.query({ type: 'image' }),
   4720 						multiple: false,
   4721 						date: false,
   4722 						priority: 20,
   4723 						suggestedWidth: this.params.width,
   4724 						suggestedHeight: this.params.height
   4725 					}),
   4726 					new wp.media.controller.SiteIconCropper({
   4727 						imgSelectOptions: this.calculateImageSelectOptions,
   4728 						control: this
   4729 					})
   4730 				]
   4731 			});
   4732 
   4733 			this.frame.on( 'select', this.onSelect, this );
   4734 			this.frame.on( 'cropped', this.onCropped, this );
   4735 			this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
   4736 		},
   4737 
   4738 		/**
   4739 		 * After an image is selected in the media modal, switch to the cropper
   4740 		 * state if the image isn't the right size.
   4741 		 */
   4742 		onSelect: function() {
   4743 			var attachment = this.frame.state().get( 'selection' ).first().toJSON(),
   4744 				controller = this;
   4745 
   4746 			if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
   4747 				wp.ajax.post( 'crop-image', {
   4748 					nonce: attachment.nonces.edit,
   4749 					id: attachment.id,
   4750 					context: 'site-icon',
   4751 					cropDetails: {
   4752 						x1: 0,
   4753 						y1: 0,
   4754 						width: this.params.width,
   4755 						height: this.params.height,
   4756 						dst_width: this.params.width,
   4757 						dst_height: this.params.height
   4758 					}
   4759 				} ).done( function( croppedImage ) {
   4760 					controller.setImageFromAttachment( croppedImage );
   4761 					controller.frame.close();
   4762 				} ).fail( function() {
   4763 					controller.frame.trigger('content:error:crop');
   4764 				} );
   4765 			} else {
   4766 				this.frame.setState( 'cropper' );
   4767 			}
   4768 		},
   4769 
   4770 		/**
   4771 		 * Updates the setting and re-renders the control UI.
   4772 		 *
   4773 		 * @param {Object} attachment
   4774 		 */
   4775 		setImageFromAttachment: function( attachment ) {
   4776 			var sizes = [ 'site_icon-32', 'thumbnail', 'full' ], link,
   4777 				icon;
   4778 
   4779 			_.each( sizes, function( size ) {
   4780 				if ( ! icon && ! _.isUndefined ( attachment.sizes[ size ] ) ) {
   4781 					icon = attachment.sizes[ size ];
   4782 				}
   4783 			} );
   4784 
   4785 			this.params.attachment = attachment;
   4786 
   4787 			// Set the Customizer setting; the callback takes care of rendering.
   4788 			this.setting( attachment.id );
   4789 
   4790 			if ( ! icon ) {
   4791 				return;
   4792 			}
   4793 
   4794 			// Update the icon in-browser.
   4795 			link = $( 'link[rel="icon"][sizes="32x32"]' );
   4796 			link.attr( 'href', icon.url );
   4797 		},
   4798 
   4799 		/**
   4800 		 * Called when the "Remove" link is clicked. Empties the setting.
   4801 		 *
   4802 		 * @param {Object} event jQuery Event object
   4803 		 */
   4804 		removeFile: function( event ) {
   4805 			if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
   4806 				return;
   4807 			}
   4808 			event.preventDefault();
   4809 
   4810 			this.params.attachment = {};
   4811 			this.setting( '' );
   4812 			this.renderContent(); // Not bound to setting change when emptying.
   4813 			$( 'link[rel="icon"][sizes="32x32"]' ).attr( 'href', '/favicon.ico' ); // Set to default.
   4814 		}
   4815 	});
   4816 
   4817 	/**
   4818 	 * @class    wp.customize.HeaderControl
   4819 	 * @augments wp.customize.Control
   4820 	 */
   4821 	api.HeaderControl = api.Control.extend(/** @lends wp.customize.HeaderControl.prototype */{
   4822 		ready: function() {
   4823 			this.btnRemove = $('#customize-control-header_image .actions .remove');
   4824 			this.btnNew    = $('#customize-control-header_image .actions .new');
   4825 
   4826 			_.bindAll(this, 'openMedia', 'removeImage');
   4827 
   4828 			this.btnNew.on( 'click', this.openMedia );
   4829 			this.btnRemove.on( 'click', this.removeImage );
   4830 
   4831 			api.HeaderTool.currentHeader = this.getInitialHeaderImage();
   4832 
   4833 			new api.HeaderTool.CurrentView({
   4834 				model: api.HeaderTool.currentHeader,
   4835 				el: '#customize-control-header_image .current .container'
   4836 			});
   4837 
   4838 			new api.HeaderTool.ChoiceListView({
   4839 				collection: api.HeaderTool.UploadsList = new api.HeaderTool.ChoiceList(),
   4840 				el: '#customize-control-header_image .choices .uploaded .list'
   4841 			});
   4842 
   4843 			new api.HeaderTool.ChoiceListView({
   4844 				collection: api.HeaderTool.DefaultsList = new api.HeaderTool.DefaultsList(),
   4845 				el: '#customize-control-header_image .choices .default .list'
   4846 			});
   4847 
   4848 			api.HeaderTool.combinedList = api.HeaderTool.CombinedList = new api.HeaderTool.CombinedList([
   4849 				api.HeaderTool.UploadsList,
   4850 				api.HeaderTool.DefaultsList
   4851 			]);
   4852 
   4853 			// Ensure custom-header-crop Ajax requests bootstrap the Customizer to activate the previewed theme.
   4854 			wp.media.controller.Cropper.prototype.defaults.doCropArgs.wp_customize = 'on';
   4855 			wp.media.controller.Cropper.prototype.defaults.doCropArgs.customize_theme = api.settings.theme.stylesheet;
   4856 		},
   4857 
   4858 		/**
   4859 		 * Returns a new instance of api.HeaderTool.ImageModel based on the currently
   4860 		 * saved header image (if any).
   4861 		 *
   4862 		 * @since 4.2.0
   4863 		 *
   4864 		 * @return {Object} Options
   4865 		 */
   4866 		getInitialHeaderImage: function() {
   4867 			if ( ! api.get().header_image || ! api.get().header_image_data || _.contains( [ 'remove-header', 'random-default-image', 'random-uploaded-image' ], api.get().header_image ) ) {
   4868 				return new api.HeaderTool.ImageModel();
   4869 			}
   4870 
   4871 			// Get the matching uploaded image object.
   4872 			var currentHeaderObject = _.find( _wpCustomizeHeader.uploads, function( imageObj ) {
   4873 				return ( imageObj.attachment_id === api.get().header_image_data.attachment_id );
   4874 			} );
   4875 			// Fall back to raw current header image.
   4876 			if ( ! currentHeaderObject ) {
   4877 				currentHeaderObject = {
   4878 					url: api.get().header_image,
   4879 					thumbnail_url: api.get().header_image,
   4880 					attachment_id: api.get().header_image_data.attachment_id
   4881 				};
   4882 			}
   4883 
   4884 			return new api.HeaderTool.ImageModel({
   4885 				header: currentHeaderObject,
   4886 				choice: currentHeaderObject.url.split( '/' ).pop()
   4887 			});
   4888 		},
   4889 
   4890 		/**
   4891 		 * Returns a set of options, computed from the attached image data and
   4892 		 * theme-specific data, to be fed to the imgAreaSelect plugin in
   4893 		 * wp.media.view.Cropper.
   4894 		 *
   4895 		 * @param {wp.media.model.Attachment} attachment
   4896 		 * @param {wp.media.controller.Cropper} controller
   4897 		 * @return {Object} Options
   4898 		 */
   4899 		calculateImageSelectOptions: function(attachment, controller) {
   4900 			var xInit = parseInt(_wpCustomizeHeader.data.width, 10),
   4901 				yInit = parseInt(_wpCustomizeHeader.data.height, 10),
   4902 				flexWidth = !! parseInt(_wpCustomizeHeader.data['flex-width'], 10),
   4903 				flexHeight = !! parseInt(_wpCustomizeHeader.data['flex-height'], 10),
   4904 				ratio, xImg, yImg, realHeight, realWidth,
   4905 				imgSelectOptions;
   4906 
   4907 			realWidth = attachment.get('width');
   4908 			realHeight = attachment.get('height');
   4909 
   4910 			this.headerImage = new api.HeaderTool.ImageModel();
   4911 			this.headerImage.set({
   4912 				themeWidth: xInit,
   4913 				themeHeight: yInit,
   4914 				themeFlexWidth: flexWidth,
   4915 				themeFlexHeight: flexHeight,
   4916 				imageWidth: realWidth,
   4917 				imageHeight: realHeight
   4918 			});
   4919 
   4920 			controller.set( 'canSkipCrop', ! this.headerImage.shouldBeCropped() );
   4921 
   4922 			ratio = xInit / yInit;
   4923 			xImg = realWidth;
   4924 			yImg = realHeight;
   4925 
   4926 			if ( xImg / yImg > ratio ) {
   4927 				yInit = yImg;
   4928 				xInit = yInit * ratio;
   4929 			} else {
   4930 				xInit = xImg;
   4931 				yInit = xInit / ratio;
   4932 			}
   4933 
   4934 			imgSelectOptions = {
   4935 				handles: true,
   4936 				keys: true,
   4937 				instance: true,
   4938 				persistent: true,
   4939 				imageWidth: realWidth,
   4940 				imageHeight: realHeight,
   4941 				x1: 0,
   4942 				y1: 0,
   4943 				x2: xInit,
   4944 				y2: yInit
   4945 			};
   4946 
   4947 			if (flexHeight === false && flexWidth === false) {
   4948 				imgSelectOptions.aspectRatio = xInit + ':' + yInit;
   4949 			}
   4950 			if (flexHeight === false ) {
   4951 				imgSelectOptions.maxHeight = yInit;
   4952 			}
   4953 			if (flexWidth === false ) {
   4954 				imgSelectOptions.maxWidth = xInit;
   4955 			}
   4956 
   4957 			return imgSelectOptions;
   4958 		},
   4959 
   4960 		/**
   4961 		 * Sets up and opens the Media Manager in order to select an image.
   4962 		 * Depending on both the size of the image and the properties of the
   4963 		 * current theme, a cropping step after selection may be required or
   4964 		 * skippable.
   4965 		 *
   4966 		 * @param {event} event
   4967 		 */
   4968 		openMedia: function(event) {
   4969 			var l10n = _wpMediaViewsL10n;
   4970 
   4971 			event.preventDefault();
   4972 
   4973 			this.frame = wp.media({
   4974 				button: {
   4975 					text: l10n.selectAndCrop,
   4976 					close: false
   4977 				},
   4978 				states: [
   4979 					new wp.media.controller.Library({
   4980 						title:     l10n.chooseImage,
   4981 						library:   wp.media.query({ type: 'image' }),
   4982 						multiple:  false,
   4983 						date:      false,
   4984 						priority:  20,
   4985 						suggestedWidth: _wpCustomizeHeader.data.width,
   4986 						suggestedHeight: _wpCustomizeHeader.data.height
   4987 					}),
   4988 					new wp.media.controller.Cropper({
   4989 						imgSelectOptions: this.calculateImageSelectOptions
   4990 					})
   4991 				]
   4992 			});
   4993 
   4994 			this.frame.on('select', this.onSelect, this);
   4995 			this.frame.on('cropped', this.onCropped, this);
   4996 			this.frame.on('skippedcrop', this.onSkippedCrop, this);
   4997 
   4998 			this.frame.open();
   4999 		},
   5000 
   5001 		/**
   5002 		 * After an image is selected in the media modal,
   5003 		 * switch to the cropper state.
   5004 		 */
   5005 		onSelect: function() {
   5006 			this.frame.setState('cropper');
   5007 		},
   5008 
   5009 		/**
   5010 		 * After the image has been cropped, apply the cropped image data to the setting.
   5011 		 *
   5012 		 * @param {Object} croppedImage Cropped attachment data.
   5013 		 */
   5014 		onCropped: function(croppedImage) {
   5015 			var url = croppedImage.url,
   5016 				attachmentId = croppedImage.attachment_id,
   5017 				w = croppedImage.width,
   5018 				h = croppedImage.height;
   5019 			this.setImageFromURL(url, attachmentId, w, h);
   5020 		},
   5021 
   5022 		/**
   5023 		 * If cropping was skipped, apply the image data directly to the setting.
   5024 		 *
   5025 		 * @param {Object} selection
   5026 		 */
   5027 		onSkippedCrop: function(selection) {
   5028 			var url = selection.get('url'),
   5029 				w = selection.get('width'),
   5030 				h = selection.get('height');
   5031 			this.setImageFromURL(url, selection.id, w, h);
   5032 		},
   5033 
   5034 		/**
   5035 		 * Creates a new wp.customize.HeaderTool.ImageModel from provided
   5036 		 * header image data and inserts it into the user-uploaded headers
   5037 		 * collection.
   5038 		 *
   5039 		 * @param {string} url
   5040 		 * @param {number} attachmentId
   5041 		 * @param {number} width
   5042 		 * @param {number} height
   5043 		 */
   5044 		setImageFromURL: function(url, attachmentId, width, height) {
   5045 			var choice, data = {};
   5046 
   5047 			data.url = url;
   5048 			data.thumbnail_url = url;
   5049 			data.timestamp = _.now();
   5050 
   5051 			if (attachmentId) {
   5052 				data.attachment_id = attachmentId;
   5053 			}
   5054 
   5055 			if (width) {
   5056 				data.width = width;
   5057 			}
   5058 
   5059 			if (height) {
   5060 				data.height = height;
   5061 			}
   5062 
   5063 			choice = new api.HeaderTool.ImageModel({
   5064 				header: data,
   5065 				choice: url.split('/').pop()
   5066 			});
   5067 			api.HeaderTool.UploadsList.add(choice);
   5068 			api.HeaderTool.currentHeader.set(choice.toJSON());
   5069 			choice.save();
   5070 			choice.importImage();
   5071 		},
   5072 
   5073 		/**
   5074 		 * Triggers the necessary events to deselect an image which was set as
   5075 		 * the currently selected one.
   5076 		 */
   5077 		removeImage: function() {
   5078 			api.HeaderTool.currentHeader.trigger('hide');
   5079 			api.HeaderTool.CombinedList.trigger('control:removeImage');
   5080 		}
   5081 
   5082 	});
   5083 
   5084 	/**
   5085 	 * wp.customize.ThemeControl
   5086 	 *
   5087 	 * @class    wp.customize.ThemeControl
   5088 	 * @augments wp.customize.Control
   5089 	 */
   5090 	api.ThemeControl = api.Control.extend(/** @lends wp.customize.ThemeControl.prototype */{
   5091 
   5092 		touchDrag: false,
   5093 		screenshotRendered: false,
   5094 
   5095 		/**
   5096 		 * @since 4.2.0
   5097 		 */
   5098 		ready: function() {
   5099 			var control = this, panel = api.panel( 'themes' );
   5100 
   5101 			function disableSwitchButtons() {
   5102 				return ! panel.canSwitchTheme( control.params.theme.id );
   5103 			}
   5104 
   5105 			// Temporary special function since supplying SFTP credentials does not work yet. See #42184.
   5106 			function disableInstallButtons() {
   5107 				return disableSwitchButtons() || false === api.settings.theme._canInstall || true === api.settings.theme._filesystemCredentialsNeeded;
   5108 			}
   5109 			function updateButtons() {
   5110 				control.container.find( 'button.preview, button.preview-theme' ).toggleClass( 'disabled', disableSwitchButtons() );
   5111 				control.container.find( 'button.theme-install' ).toggleClass( 'disabled', disableInstallButtons() );
   5112 			}
   5113 
   5114 			api.state( 'selectedChangesetStatus' ).bind( updateButtons );
   5115 			api.state( 'changesetStatus' ).bind( updateButtons );
   5116 			updateButtons();
   5117 
   5118 			control.container.on( 'touchmove', '.theme', function() {
   5119 				control.touchDrag = true;
   5120 			});
   5121 
   5122 			// Bind details view trigger.
   5123 			control.container.on( 'click keydown touchend', '.theme', function( event ) {
   5124 				var section;
   5125 				if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
   5126 					return;
   5127 				}
   5128 
   5129 				// Bail if the user scrolled on a touch device.
   5130 				if ( control.touchDrag === true ) {
   5131 					return control.touchDrag = false;
   5132 				}
   5133 
   5134 				// Prevent the modal from showing when the user clicks the action button.
   5135 				if ( $( event.target ).is( '.theme-actions .button, .update-theme' ) ) {
   5136 					return;
   5137 				}
   5138 
   5139 				event.preventDefault(); // Keep this AFTER the key filter above.
   5140 				section = api.section( control.section() );
   5141 				section.showDetails( control.params.theme, function() {
   5142 
   5143 					// Temporary special function since supplying SFTP credentials does not work yet. See #42184.
   5144 					if ( api.settings.theme._filesystemCredentialsNeeded ) {
   5145 						section.overlay.find( '.theme-actions .delete-theme' ).remove();
   5146 					}
   5147 				} );
   5148 			});
   5149 
   5150 			control.container.on( 'render-screenshot', function() {
   5151 				var $screenshot = $( this ).find( 'img' ),
   5152 					source = $screenshot.data( 'src' );
   5153 
   5154 				if ( source ) {
   5155 					$screenshot.attr( 'src', source );
   5156 				}
   5157 				control.screenshotRendered = true;
   5158 			});
   5159 		},
   5160 
   5161 		/**
   5162 		 * Show or hide the theme based on the presence of the term in the title, description, tags, and author.
   5163 		 *
   5164 		 * @since 4.2.0
   5165 		 * @param {Array} terms - An array of terms to search for.
   5166 		 * @return {boolean} Whether a theme control was activated or not.
   5167 		 */
   5168 		filter: function( terms ) {
   5169 			var control = this,
   5170 				matchCount = 0,
   5171 				haystack = control.params.theme.name + ' ' +
   5172 					control.params.theme.description + ' ' +
   5173 					control.params.theme.tags + ' ' +
   5174 					control.params.theme.author + ' ';
   5175 			haystack = haystack.toLowerCase().replace( '-', ' ' );
   5176 
   5177 			// Back-compat for behavior in WordPress 4.2.0 to 4.8.X.
   5178 			if ( ! _.isArray( terms ) ) {
   5179 				terms = [ terms ];
   5180 			}
   5181 
   5182 			// Always give exact name matches highest ranking.
   5183 			if ( control.params.theme.name.toLowerCase() === terms.join( ' ' ) ) {
   5184 				matchCount = 100;
   5185 			} else {
   5186 
   5187 				// Search for and weight (by 10) complete term matches.
   5188 				matchCount = matchCount + 10 * ( haystack.split( terms.join( ' ' ) ).length - 1 );
   5189 
   5190 				// Search for each term individually (as whole-word and partial match) and sum weighted match counts.
   5191 				_.each( terms, function( term ) {
   5192 					matchCount = matchCount + 2 * ( haystack.split( term + ' ' ).length - 1 ); // Whole-word, double-weighted.
   5193 					matchCount = matchCount + haystack.split( term ).length - 1; // Partial word, to minimize empty intermediate searches while typing.
   5194 				});
   5195 
   5196 				// Upper limit on match ranking.
   5197 				if ( matchCount > 99 ) {
   5198 					matchCount = 99;
   5199 				}
   5200 			}
   5201 
   5202 			if ( 0 !== matchCount ) {
   5203 				control.activate();
   5204 				control.params.priority = 101 - matchCount; // Sort results by match count.
   5205 				return true;
   5206 			} else {
   5207 				control.deactivate(); // Hide control.
   5208 				control.params.priority = 101;
   5209 				return false;
   5210 			}
   5211 		},
   5212 
   5213 		/**
   5214 		 * Rerender the theme from its JS template with the installed type.
   5215 		 *
   5216 		 * @since 4.9.0
   5217 		 *
   5218 		 * @return {void}
   5219 		 */
   5220 		rerenderAsInstalled: function( installed ) {
   5221 			var control = this, section;
   5222 			if ( installed ) {
   5223 				control.params.theme.type = 'installed';
   5224 			} else {
   5225 				section = api.section( control.params.section );
   5226 				control.params.theme.type = section.params.action;
   5227 			}
   5228 			control.renderContent(); // Replaces existing content.
   5229 			control.container.trigger( 'render-screenshot' );
   5230 		}
   5231 	});
   5232 
   5233 	/**
   5234 	 * Class wp.customize.CodeEditorControl
   5235 	 *
   5236 	 * @since 4.9.0
   5237 	 *
   5238 	 * @class    wp.customize.CodeEditorControl
   5239 	 * @augments wp.customize.Control
   5240 	 */
   5241 	api.CodeEditorControl = api.Control.extend(/** @lends wp.customize.CodeEditorControl.prototype */{
   5242 
   5243 		/**
   5244 		 * Initialize.
   5245 		 *
   5246 		 * @since 4.9.0
   5247 		 * @param {string} id      - Unique identifier for the control instance.
   5248 		 * @param {Object} options - Options hash for the control instance.
   5249 		 * @return {void}
   5250 		 */
   5251 		initialize: function( id, options ) {
   5252 			var control = this;
   5253 			control.deferred = _.extend( control.deferred || {}, {
   5254 				codemirror: $.Deferred()
   5255 			} );
   5256 			api.Control.prototype.initialize.call( control, id, options );
   5257 
   5258 			// Note that rendering is debounced so the props will be used when rendering happens after add event.
   5259 			control.notifications.bind( 'add', function( notification ) {
   5260 
   5261 				// Skip if control notification is not from setting csslint_error notification.
   5262 				if ( notification.code !== control.setting.id + ':csslint_error' ) {
   5263 					return;
   5264 				}
   5265 
   5266 				// Customize the template and behavior of csslint_error notifications.
   5267 				notification.templateId = 'customize-code-editor-lint-error-notification';
   5268 				notification.render = (function( render ) {
   5269 					return function() {
   5270 						var li = render.call( this );
   5271 						li.find( 'input[type=checkbox]' ).on( 'click', function() {
   5272 							control.setting.notifications.remove( 'csslint_error' );
   5273 						} );
   5274 						return li;
   5275 					};
   5276 				})( notification.render );
   5277 			} );
   5278 		},
   5279 
   5280 		/**
   5281 		 * Initialize the editor when the containing section is ready and expanded.
   5282 		 *
   5283 		 * @since 4.9.0
   5284 		 * @return {void}
   5285 		 */
   5286 		ready: function() {
   5287 			var control = this;
   5288 			if ( ! control.section() ) {
   5289 				control.initEditor();
   5290 				return;
   5291 			}
   5292 
   5293 			// Wait to initialize editor until section is embedded and expanded.
   5294 			api.section( control.section(), function( section ) {
   5295 				section.deferred.embedded.done( function() {
   5296 					var onceExpanded;
   5297 					if ( section.expanded() ) {
   5298 						control.initEditor();
   5299 					} else {
   5300 						onceExpanded = function( isExpanded ) {
   5301 							if ( isExpanded ) {
   5302 								control.initEditor();
   5303 								section.expanded.unbind( onceExpanded );
   5304 							}
   5305 						};
   5306 						section.expanded.bind( onceExpanded );
   5307 					}
   5308 				} );
   5309 			} );
   5310 		},
   5311 
   5312 		/**
   5313 		 * Initialize editor.
   5314 		 *
   5315 		 * @since 4.9.0
   5316 		 * @return {void}
   5317 		 */
   5318 		initEditor: function() {
   5319 			var control = this, element, editorSettings = false;
   5320 
   5321 			// Obtain editorSettings for instantiation.
   5322 			if ( wp.codeEditor && ( _.isUndefined( control.params.editor_settings ) || false !== control.params.editor_settings ) ) {
   5323 
   5324 				// Obtain default editor settings.
   5325 				editorSettings = wp.codeEditor.defaultSettings ? _.clone( wp.codeEditor.defaultSettings ) : {};
   5326 				editorSettings.codemirror = _.extend(
   5327 					{},
   5328 					editorSettings.codemirror,
   5329 					{
   5330 						indentUnit: 2,
   5331 						tabSize: 2
   5332 					}
   5333 				);
   5334 
   5335 				// Merge editor_settings param on top of defaults.
   5336 				if ( _.isObject( control.params.editor_settings ) ) {
   5337 					_.each( control.params.editor_settings, function( value, key ) {
   5338 						if ( _.isObject( value ) ) {
   5339 							editorSettings[ key ] = _.extend(
   5340 								{},
   5341 								editorSettings[ key ],
   5342 								value
   5343 							);
   5344 						}
   5345 					} );
   5346 				}
   5347 			}
   5348 
   5349 			element = new api.Element( control.container.find( 'textarea' ) );
   5350 			control.elements.push( element );
   5351 			element.sync( control.setting );
   5352 			element.set( control.setting() );
   5353 
   5354 			if ( editorSettings ) {
   5355 				control.initSyntaxHighlightingEditor( editorSettings );
   5356 			} else {
   5357 				control.initPlainTextareaEditor();
   5358 			}
   5359 		},
   5360 
   5361 		/**
   5362 		 * Make sure editor gets focused when control is focused.
   5363 		 *
   5364 		 * @since 4.9.0
   5365 		 * @param {Object}   [params] - Focus params.
   5366 		 * @param {Function} [params.completeCallback] - Function to call when expansion is complete.
   5367 		 * @return {void}
   5368 		 */
   5369 		focus: function( params ) {
   5370 			var control = this, extendedParams = _.extend( {}, params ), originalCompleteCallback;
   5371 			originalCompleteCallback = extendedParams.completeCallback;
   5372 			extendedParams.completeCallback = function() {
   5373 				if ( originalCompleteCallback ) {
   5374 					originalCompleteCallback();
   5375 				}
   5376 				if ( control.editor ) {
   5377 					control.editor.codemirror.focus();
   5378 				}
   5379 			};
   5380 			api.Control.prototype.focus.call( control, extendedParams );
   5381 		},
   5382 
   5383 		/**
   5384 		 * Initialize syntax-highlighting editor.
   5385 		 *
   5386 		 * @since 4.9.0
   5387 		 * @param {Object} codeEditorSettings - Code editor settings.
   5388 		 * @return {void}
   5389 		 */
   5390 		initSyntaxHighlightingEditor: function( codeEditorSettings ) {
   5391 			var control = this, $textarea = control.container.find( 'textarea' ), settings, suspendEditorUpdate = false;
   5392 
   5393 			settings = _.extend( {}, codeEditorSettings, {
   5394 				onTabNext: _.bind( control.onTabNext, control ),
   5395 				onTabPrevious: _.bind( control.onTabPrevious, control ),
   5396 				onUpdateErrorNotice: _.bind( control.onUpdateErrorNotice, control )
   5397 			});
   5398 
   5399 			control.editor = wp.codeEditor.initialize( $textarea, settings );
   5400 
   5401 			// Improve the editor accessibility.
   5402 			$( control.editor.codemirror.display.lineDiv )
   5403 				.attr({
   5404 					role: 'textbox',
   5405 					'aria-multiline': 'true',
   5406 					'aria-label': control.params.label,
   5407 					'aria-describedby': 'editor-keyboard-trap-help-1 editor-keyboard-trap-help-2 editor-keyboard-trap-help-3 editor-keyboard-trap-help-4'
   5408 				});
   5409 
   5410 			// Focus the editor when clicking on its label.
   5411 			control.container.find( 'label' ).on( 'click', function() {
   5412 				control.editor.codemirror.focus();
   5413 			});
   5414 
   5415 			/*
   5416 			 * When the CodeMirror instance changes, mirror to the textarea,
   5417 			 * where we have our "true" change event handler bound.
   5418 			 */
   5419 			control.editor.codemirror.on( 'change', function( codemirror ) {
   5420 				suspendEditorUpdate = true;
   5421 				$textarea.val( codemirror.getValue() ).trigger( 'change' );
   5422 				suspendEditorUpdate = false;
   5423 			});
   5424 
   5425 			// Update CodeMirror when the setting is changed by another plugin.
   5426 			control.setting.bind( function( value ) {
   5427 				if ( ! suspendEditorUpdate ) {
   5428 					control.editor.codemirror.setValue( value );
   5429 				}
   5430 			});
   5431 
   5432 			// Prevent collapsing section when hitting Esc to tab out of editor.
   5433 			control.editor.codemirror.on( 'keydown', function onKeydown( codemirror, event ) {
   5434 				var escKeyCode = 27;
   5435 				if ( escKeyCode === event.keyCode ) {
   5436 					event.stopPropagation();
   5437 				}
   5438 			});
   5439 
   5440 			control.deferred.codemirror.resolveWith( control, [ control.editor.codemirror ] );
   5441 		},
   5442 
   5443 		/**
   5444 		 * Handle tabbing to the field after the editor.
   5445 		 *
   5446 		 * @since 4.9.0
   5447 		 * @return {void}
   5448 		 */
   5449 		onTabNext: function onTabNext() {
   5450 			var control = this, controls, controlIndex, section;
   5451 			section = api.section( control.section() );
   5452 			controls = section.controls();
   5453 			controlIndex = controls.indexOf( control );
   5454 			if ( controls.length === controlIndex + 1 ) {
   5455 				$( '#customize-footer-actions .collapse-sidebar' ).trigger( 'focus' );
   5456 			} else {
   5457 				controls[ controlIndex + 1 ].container.find( ':focusable:first' ).focus();
   5458 			}
   5459 		},
   5460 
   5461 		/**
   5462 		 * Handle tabbing to the field before the editor.
   5463 		 *
   5464 		 * @since 4.9.0
   5465 		 * @return {void}
   5466 		 */
   5467 		onTabPrevious: function onTabPrevious() {
   5468 			var control = this, controls, controlIndex, section;
   5469 			section = api.section( control.section() );
   5470 			controls = section.controls();
   5471 			controlIndex = controls.indexOf( control );
   5472 			if ( 0 === controlIndex ) {
   5473 				section.contentContainer.find( '.customize-section-title .customize-help-toggle, .customize-section-title .customize-section-description.open .section-description-close' ).last().focus();
   5474 			} else {
   5475 				controls[ controlIndex - 1 ].contentContainer.find( ':focusable:first' ).focus();
   5476 			}
   5477 		},
   5478 
   5479 		/**
   5480 		 * Update error notice.
   5481 		 *
   5482 		 * @since 4.9.0
   5483 		 * @param {Array} errorAnnotations - Error annotations.
   5484 		 * @return {void}
   5485 		 */
   5486 		onUpdateErrorNotice: function onUpdateErrorNotice( errorAnnotations ) {
   5487 			var control = this, message;
   5488 			control.setting.notifications.remove( 'csslint_error' );
   5489 
   5490 			if ( 0 !== errorAnnotations.length ) {
   5491 				if ( 1 === errorAnnotations.length ) {
   5492 					message = api.l10n.customCssError.singular.replace( '%d', '1' );
   5493 				} else {
   5494 					message = api.l10n.customCssError.plural.replace( '%d', String( errorAnnotations.length ) );
   5495 				}
   5496 				control.setting.notifications.add( new api.Notification( 'csslint_error', {
   5497 					message: message,
   5498 					type: 'error'
   5499 				} ) );
   5500 			}
   5501 		},
   5502 
   5503 		/**
   5504 		 * Initialize plain-textarea editor when syntax highlighting is disabled.
   5505 		 *
   5506 		 * @since 4.9.0
   5507 		 * @return {void}
   5508 		 */
   5509 		initPlainTextareaEditor: function() {
   5510 			var control = this, $textarea = control.container.find( 'textarea' ), textarea = $textarea[0];
   5511 
   5512 			$textarea.on( 'blur', function onBlur() {
   5513 				$textarea.data( 'next-tab-blurs', false );
   5514 			} );
   5515 
   5516 			$textarea.on( 'keydown', function onKeydown( event ) {
   5517 				var selectionStart, selectionEnd, value, tabKeyCode = 9, escKeyCode = 27;
   5518 
   5519 				if ( escKeyCode === event.keyCode ) {
   5520 					if ( ! $textarea.data( 'next-tab-blurs' ) ) {
   5521 						$textarea.data( 'next-tab-blurs', true );
   5522 						event.stopPropagation(); // Prevent collapsing the section.
   5523 					}
   5524 					return;
   5525 				}
   5526 
   5527 				// Short-circuit if tab key is not being pressed or if a modifier key *is* being pressed.
   5528 				if ( tabKeyCode !== event.keyCode || event.ctrlKey || event.altKey || event.shiftKey ) {
   5529 					return;
   5530 				}
   5531 
   5532 				// Prevent capturing Tab characters if Esc was pressed.
   5533 				if ( $textarea.data( 'next-tab-blurs' ) ) {
   5534 					return;
   5535 				}
   5536 
   5537 				selectionStart = textarea.selectionStart;
   5538 				selectionEnd = textarea.selectionEnd;
   5539 				value = textarea.value;
   5540 
   5541 				if ( selectionStart >= 0 ) {
   5542 					textarea.value = value.substring( 0, selectionStart ).concat( '\t', value.substring( selectionEnd ) );
   5543 					$textarea.selectionStart = textarea.selectionEnd = selectionStart + 1;
   5544 				}
   5545 
   5546 				event.stopPropagation();
   5547 				event.preventDefault();
   5548 			});
   5549 
   5550 			control.deferred.codemirror.rejectWith( control );
   5551 		}
   5552 	});
   5553 
   5554 	/**
   5555 	 * Class wp.customize.DateTimeControl.
   5556 	 *
   5557 	 * @since 4.9.0
   5558 	 * @class    wp.customize.DateTimeControl
   5559 	 * @augments wp.customize.Control
   5560 	 */
   5561 	api.DateTimeControl = api.Control.extend(/** @lends wp.customize.DateTimeControl.prototype */{
   5562 
   5563 		/**
   5564 		 * Initialize behaviors.
   5565 		 *
   5566 		 * @since 4.9.0
   5567 		 * @return {void}
   5568 		 */
   5569 		ready: function ready() {
   5570 			var control = this;
   5571 
   5572 			control.inputElements = {};
   5573 			control.invalidDate = false;
   5574 
   5575 			_.bindAll( control, 'populateSetting', 'updateDaysForMonth', 'populateDateInputs' );
   5576 
   5577 			if ( ! control.setting ) {
   5578 				throw new Error( 'Missing setting' );
   5579 			}
   5580 
   5581 			control.container.find( '.date-input' ).each( function() {
   5582 				var input = $( this ), component, element;
   5583 				component = input.data( 'component' );
   5584 				element = new api.Element( input );
   5585 				control.inputElements[ component ] = element;
   5586 				control.elements.push( element );
   5587 
   5588 				// Add invalid date error once user changes (and has blurred the input).
   5589 				input.on( 'change', function() {
   5590 					if ( control.invalidDate ) {
   5591 						control.notifications.add( new api.Notification( 'invalid_date', {
   5592 							message: api.l10n.invalidDate
   5593 						} ) );
   5594 					}
   5595 				} );
   5596 
   5597 				// Remove the error immediately after validity change.
   5598 				input.on( 'input', _.debounce( function() {
   5599 					if ( ! control.invalidDate ) {
   5600 						control.notifications.remove( 'invalid_date' );
   5601 					}
   5602 				} ) );
   5603 
   5604 				// Add zero-padding when blurring field.
   5605 				input.on( 'blur', _.debounce( function() {
   5606 					if ( ! control.invalidDate ) {
   5607 						control.populateDateInputs();
   5608 					}
   5609 				} ) );
   5610 			} );
   5611 
   5612 			control.inputElements.month.bind( control.updateDaysForMonth );
   5613 			control.inputElements.year.bind( control.updateDaysForMonth );
   5614 			control.populateDateInputs();
   5615 			control.setting.bind( control.populateDateInputs );
   5616 
   5617 			// Start populating setting after inputs have been populated.
   5618 			_.each( control.inputElements, function( element ) {
   5619 				element.bind( control.populateSetting );
   5620 			} );
   5621 		},
   5622 
   5623 		/**
   5624 		 * Parse datetime string.
   5625 		 *
   5626 		 * @since 4.9.0
   5627 		 *
   5628 		 * @param {string} datetime - Date/Time string. Accepts Y-m-d[ H:i[:s]] format.
   5629 		 * @return {Object|null} Returns object containing date components or null if parse error.
   5630 		 */
   5631 		parseDateTime: function parseDateTime( datetime ) {
   5632 			var control = this, matches, date, midDayHour = 12;
   5633 
   5634 			if ( datetime ) {
   5635 				matches = datetime.match( /^(\d\d\d\d)-(\d\d)-(\d\d)(?: (\d\d):(\d\d)(?::(\d\d))?)?$/ );
   5636 			}
   5637 
   5638 			if ( ! matches ) {
   5639 				return null;
   5640 			}
   5641 
   5642 			matches.shift();
   5643 
   5644 			date = {
   5645 				year: matches.shift(),
   5646 				month: matches.shift(),
   5647 				day: matches.shift(),
   5648 				hour: matches.shift() || '00',
   5649 				minute: matches.shift() || '00',
   5650 				second: matches.shift() || '00'
   5651 			};
   5652 
   5653 			if ( control.params.includeTime && control.params.twelveHourFormat ) {
   5654 				date.hour = parseInt( date.hour, 10 );
   5655 				date.meridian = date.hour >= midDayHour ? 'pm' : 'am';
   5656 				date.hour = date.hour % midDayHour ? String( date.hour % midDayHour ) : String( midDayHour );
   5657 				delete date.second; // @todo Why only if twelveHourFormat?
   5658 			}
   5659 
   5660 			return date;
   5661 		},
   5662 
   5663 		/**
   5664 		 * Validates if input components have valid date and time.
   5665 		 *
   5666 		 * @since 4.9.0
   5667 		 * @return {boolean} If date input fields has error.
   5668 		 */
   5669 		validateInputs: function validateInputs() {
   5670 			var control = this, components, validityInput;
   5671 
   5672 			control.invalidDate = false;
   5673 
   5674 			components = [ 'year', 'day' ];
   5675 			if ( control.params.includeTime ) {
   5676 				components.push( 'hour', 'minute' );
   5677 			}
   5678 
   5679 			_.find( components, function( component ) {
   5680 				var element, max, min, value;
   5681 
   5682 				element = control.inputElements[ component ];
   5683 				validityInput = element.element.get( 0 );
   5684 				max = parseInt( element.element.attr( 'max' ), 10 );
   5685 				min = parseInt( element.element.attr( 'min' ), 10 );
   5686 				value = parseInt( element(), 10 );
   5687 				control.invalidDate = isNaN( value ) || value > max || value < min;
   5688 
   5689 				if ( ! control.invalidDate ) {
   5690 					validityInput.setCustomValidity( '' );
   5691 				}
   5692 
   5693 				return control.invalidDate;
   5694 			} );
   5695 
   5696 			if ( control.inputElements.meridian && ! control.invalidDate ) {
   5697 				validityInput = control.inputElements.meridian.element.get( 0 );
   5698 				if ( 'am' !== control.inputElements.meridian.get() && 'pm' !== control.inputElements.meridian.get() ) {
   5699 					control.invalidDate = true;
   5700 				} else {
   5701 					validityInput.setCustomValidity( '' );
   5702 				}
   5703 			}
   5704 
   5705 			if ( control.invalidDate ) {
   5706 				validityInput.setCustomValidity( api.l10n.invalidValue );
   5707 			} else {
   5708 				validityInput.setCustomValidity( '' );
   5709 			}
   5710 			if ( ! control.section() || api.section.has( control.section() ) && api.section( control.section() ).expanded() ) {
   5711 				_.result( validityInput, 'reportValidity' );
   5712 			}
   5713 
   5714 			return control.invalidDate;
   5715 		},
   5716 
   5717 		/**
   5718 		 * Updates number of days according to the month and year selected.
   5719 		 *
   5720 		 * @since 4.9.0
   5721 		 * @return {void}
   5722 		 */
   5723 		updateDaysForMonth: function updateDaysForMonth() {
   5724 			var control = this, daysInMonth, year, month, day;
   5725 
   5726 			month = parseInt( control.inputElements.month(), 10 );
   5727 			year = parseInt( control.inputElements.year(), 10 );
   5728 			day = parseInt( control.inputElements.day(), 10 );
   5729 
   5730 			if ( month && year ) {
   5731 				daysInMonth = new Date( year, month, 0 ).getDate();
   5732 				control.inputElements.day.element.attr( 'max', daysInMonth );
   5733 
   5734 				if ( day > daysInMonth ) {
   5735 					control.inputElements.day( String( daysInMonth ) );
   5736 				}
   5737 			}
   5738 		},
   5739 
   5740 		/**
   5741 		 * Populate setting value from the inputs.
   5742 		 *
   5743 		 * @since 4.9.0
   5744 		 * @return {boolean} If setting updated.
   5745 		 */
   5746 		populateSetting: function populateSetting() {
   5747 			var control = this, date;
   5748 
   5749 			if ( control.validateInputs() || ! control.params.allowPastDate && ! control.isFutureDate() ) {
   5750 				return false;
   5751 			}
   5752 
   5753 			date = control.convertInputDateToString();
   5754 			control.setting.set( date );
   5755 			return true;
   5756 		},
   5757 
   5758 		/**
   5759 		 * Converts input values to string in Y-m-d H:i:s format.
   5760 		 *
   5761 		 * @since 4.9.0
   5762 		 * @return {string} Date string.
   5763 		 */
   5764 		convertInputDateToString: function convertInputDateToString() {
   5765 			var control = this, date = '', dateFormat, hourInTwentyFourHourFormat,
   5766 				getElementValue, pad;
   5767 
   5768 			pad = function( number, padding ) {
   5769 				var zeros;
   5770 				if ( String( number ).length < padding ) {
   5771 					zeros = padding - String( number ).length;
   5772 					number = Math.pow( 10, zeros ).toString().substr( 1 ) + String( number );
   5773 				}
   5774 				return number;
   5775 			};
   5776 
   5777 			getElementValue = function( component ) {
   5778 				var value = parseInt( control.inputElements[ component ].get(), 10 );
   5779 
   5780 				if ( _.contains( [ 'month', 'day', 'hour', 'minute' ], component ) ) {
   5781 					value = pad( value, 2 );
   5782 				} else if ( 'year' === component ) {
   5783 					value = pad( value, 4 );
   5784 				}
   5785 				return value;
   5786 			};
   5787 
   5788 			dateFormat = [ 'year', '-', 'month', '-', 'day' ];
   5789 			if ( control.params.includeTime ) {
   5790 				hourInTwentyFourHourFormat = control.inputElements.meridian ? control.convertHourToTwentyFourHourFormat( control.inputElements.hour(), control.inputElements.meridian() ) : control.inputElements.hour();
   5791 				dateFormat = dateFormat.concat( [ ' ', pad( hourInTwentyFourHourFormat, 2 ), ':', 'minute', ':', '00' ] );
   5792 			}
   5793 
   5794 			_.each( dateFormat, function( component ) {
   5795 				date += control.inputElements[ component ] ? getElementValue( component ) : component;
   5796 			} );
   5797 
   5798 			return date;
   5799 		},
   5800 
   5801 		/**
   5802 		 * Check if the date is in the future.
   5803 		 *
   5804 		 * @since 4.9.0
   5805 		 * @return {boolean} True if future date.
   5806 		 */
   5807 		isFutureDate: function isFutureDate() {
   5808 			var control = this;
   5809 			return 0 < api.utils.getRemainingTime( control.convertInputDateToString() );
   5810 		},
   5811 
   5812 		/**
   5813 		 * Convert hour in twelve hour format to twenty four hour format.
   5814 		 *
   5815 		 * @since 4.9.0
   5816 		 * @param {string} hourInTwelveHourFormat - Hour in twelve hour format.
   5817 		 * @param {string} meridian - Either 'am' or 'pm'.
   5818 		 * @return {string} Hour in twenty four hour format.
   5819 		 */
   5820 		convertHourToTwentyFourHourFormat: function convertHour( hourInTwelveHourFormat, meridian ) {
   5821 			var hourInTwentyFourHourFormat, hour, midDayHour = 12;
   5822 
   5823 			hour = parseInt( hourInTwelveHourFormat, 10 );
   5824 			if ( isNaN( hour ) ) {
   5825 				return '';
   5826 			}
   5827 
   5828 			if ( 'pm' === meridian && hour < midDayHour ) {
   5829 				hourInTwentyFourHourFormat = hour + midDayHour;
   5830 			} else if ( 'am' === meridian && midDayHour === hour ) {
   5831 				hourInTwentyFourHourFormat = hour - midDayHour;
   5832 			} else {
   5833 				hourInTwentyFourHourFormat = hour;
   5834 			}
   5835 
   5836 			return String( hourInTwentyFourHourFormat );
   5837 		},
   5838 
   5839 		/**
   5840 		 * Populates date inputs in date fields.
   5841 		 *
   5842 		 * @since 4.9.0
   5843 		 * @return {boolean} Whether the inputs were populated.
   5844 		 */
   5845 		populateDateInputs: function populateDateInputs() {
   5846 			var control = this, parsed;
   5847 
   5848 			parsed = control.parseDateTime( control.setting.get() );
   5849 
   5850 			if ( ! parsed ) {
   5851 				return false;
   5852 			}
   5853 
   5854 			_.each( control.inputElements, function( element, component ) {
   5855 				var value = parsed[ component ]; // This will be zero-padded string.
   5856 
   5857 				// Set month and meridian regardless of focused state since they are dropdowns.
   5858 				if ( 'month' === component || 'meridian' === component ) {
   5859 
   5860 					// Options in dropdowns are not zero-padded.
   5861 					value = value.replace( /^0/, '' );
   5862 
   5863 					element.set( value );
   5864 				} else {
   5865 
   5866 					value = parseInt( value, 10 );
   5867 					if ( ! element.element.is( document.activeElement ) ) {
   5868 
   5869 						// Populate element with zero-padded value if not focused.
   5870 						element.set( parsed[ component ] );
   5871 					} else if ( value !== parseInt( element(), 10 ) ) {
   5872 
   5873 						// Forcibly update the value if its underlying value changed, regardless of zero-padding.
   5874 						element.set( String( value ) );
   5875 					}
   5876 				}
   5877 			} );
   5878 
   5879 			return true;
   5880 		},
   5881 
   5882 		/**
   5883 		 * Toggle future date notification for date control.
   5884 		 *
   5885 		 * @since 4.9.0
   5886 		 * @param {boolean} notify Add or remove the notification.
   5887 		 * @return {wp.customize.DateTimeControl}
   5888 		 */
   5889 		toggleFutureDateNotification: function toggleFutureDateNotification( notify ) {
   5890 			var control = this, notificationCode, notification;
   5891 
   5892 			notificationCode = 'not_future_date';
   5893 
   5894 			if ( notify ) {
   5895 				notification = new api.Notification( notificationCode, {
   5896 					type: 'error',
   5897 					message: api.l10n.futureDateError
   5898 				} );
   5899 				control.notifications.add( notification );
   5900 			} else {
   5901 				control.notifications.remove( notificationCode );
   5902 			}
   5903 
   5904 			return control;
   5905 		}
   5906 	});
   5907 
   5908 	/**
   5909 	 * Class PreviewLinkControl.
   5910 	 *
   5911 	 * @since 4.9.0
   5912 	 * @class    wp.customize.PreviewLinkControl
   5913 	 * @augments wp.customize.Control
   5914 	 */
   5915 	api.PreviewLinkControl = api.Control.extend(/** @lends wp.customize.PreviewLinkControl.prototype */{
   5916 
   5917 		defaults: _.extend( {}, api.Control.prototype.defaults, {
   5918 			templateId: 'customize-preview-link-control'
   5919 		} ),
   5920 
   5921 		/**
   5922 		 * Initialize behaviors.
   5923 		 *
   5924 		 * @since 4.9.0
   5925 		 * @return {void}
   5926 		 */
   5927 		ready: function ready() {
   5928 			var control = this, element, component, node, url, input, button;
   5929 
   5930 			_.bindAll( control, 'updatePreviewLink' );
   5931 
   5932 			if ( ! control.setting ) {
   5933 			    control.setting = new api.Value();
   5934 			}
   5935 
   5936 			control.previewElements = {};
   5937 
   5938 			control.container.find( '.preview-control-element' ).each( function() {
   5939 				node = $( this );
   5940 				component = node.data( 'component' );
   5941 				element = new api.Element( node );
   5942 				control.previewElements[ component ] = element;
   5943 				control.elements.push( element );
   5944 			} );
   5945 
   5946 			url = control.previewElements.url;
   5947 			input = control.previewElements.input;
   5948 			button = control.previewElements.button;
   5949 
   5950 			input.link( control.setting );
   5951 			url.link( control.setting );
   5952 
   5953 			url.bind( function( value ) {
   5954 				url.element.parent().attr( {
   5955 					href: value,
   5956 					target: api.settings.changeset.uuid
   5957 				} );
   5958 			} );
   5959 
   5960 			api.bind( 'ready', control.updatePreviewLink );
   5961 			api.state( 'saved' ).bind( control.updatePreviewLink );
   5962 			api.state( 'changesetStatus' ).bind( control.updatePreviewLink );
   5963 			api.state( 'activated' ).bind( control.updatePreviewLink );
   5964 			api.previewer.previewUrl.bind( control.updatePreviewLink );
   5965 
   5966 			button.element.on( 'click', function( event ) {
   5967 				event.preventDefault();
   5968 				if ( control.setting() ) {
   5969 					input.element.select();
   5970 					document.execCommand( 'copy' );
   5971 					button( button.element.data( 'copied-text' ) );
   5972 				}
   5973 			} );
   5974 
   5975 			url.element.parent().on( 'click', function( event ) {
   5976 				if ( $( this ).hasClass( 'disabled' ) ) {
   5977 					event.preventDefault();
   5978 				}
   5979 			} );
   5980 
   5981 			button.element.on( 'mouseenter', function() {
   5982 				if ( control.setting() ) {
   5983 					button( button.element.data( 'copy-text' ) );
   5984 				}
   5985 			} );
   5986 		},
   5987 
   5988 		/**
   5989 		 * Updates Preview Link
   5990 		 *
   5991 		 * @since 4.9.0
   5992 		 * @return {void}
   5993 		 */
   5994 		updatePreviewLink: function updatePreviewLink() {
   5995 			var control = this, unsavedDirtyValues;
   5996 
   5997 			unsavedDirtyValues = ! api.state( 'saved' ).get() || '' === api.state( 'changesetStatus' ).get() || 'auto-draft' === api.state( 'changesetStatus' ).get();
   5998 
   5999 			control.toggleSaveNotification( unsavedDirtyValues );
   6000 			control.previewElements.url.element.parent().toggleClass( 'disabled', unsavedDirtyValues );
   6001 			control.previewElements.button.element.prop( 'disabled', unsavedDirtyValues );
   6002 			control.setting.set( api.previewer.getFrontendPreviewUrl() );
   6003 		},
   6004 
   6005 		/**
   6006 		 * Toggles save notification.
   6007 		 *
   6008 		 * @since 4.9.0
   6009 		 * @param {boolean} notify Add or remove notification.
   6010 		 * @return {void}
   6011 		 */
   6012 		toggleSaveNotification: function toggleSaveNotification( notify ) {
   6013 			var control = this, notificationCode, notification;
   6014 
   6015 			notificationCode = 'changes_not_saved';
   6016 
   6017 			if ( notify ) {
   6018 				notification = new api.Notification( notificationCode, {
   6019 					type: 'info',
   6020 					message: api.l10n.saveBeforeShare
   6021 				} );
   6022 				control.notifications.add( notification );
   6023 			} else {
   6024 				control.notifications.remove( notificationCode );
   6025 			}
   6026 		}
   6027 	});
   6028 
   6029 	/**
   6030 	 * Change objects contained within the main customize object to Settings.
   6031 	 *
   6032 	 * @alias wp.customize.defaultConstructor
   6033 	 */
   6034 	api.defaultConstructor = api.Setting;
   6035 
   6036 	/**
   6037 	 * Callback for resolved controls.
   6038 	 *
   6039 	 * @callback wp.customize.deferredControlsCallback
   6040 	 * @param {wp.customize.Control[]} controls Resolved controls.
   6041 	 */
   6042 
   6043 	/**
   6044 	 * Collection of all registered controls.
   6045 	 *
   6046 	 * @alias wp.customize.control
   6047 	 *
   6048 	 * @since 3.4.0
   6049 	 *
   6050 	 * @type {Function}
   6051 	 * @param {...string} ids - One or more ids for controls to obtain.
   6052 	 * @param {deferredControlsCallback} [callback] - Function called when all supplied controls exist.
   6053 	 * @return {wp.customize.Control|undefined|jQuery.promise} Control instance or undefined (if function called with one id param),
   6054 	 *                                                         or promise resolving to requested controls.
   6055 	 *
   6056 	 * @example <caption>Loop over all registered controls.</caption>
   6057 	 * wp.customize.control.each( function( control ) { ... } );
   6058 	 *
   6059 	 * @example <caption>Getting `background_color` control instance.</caption>
   6060 	 * control = wp.customize.control( 'background_color' );
   6061 	 *
   6062 	 * @example <caption>Check if control exists.</caption>
   6063 	 * hasControl = wp.customize.control.has( 'background_color' );
   6064 	 *
   6065 	 * @example <caption>Deferred getting of `background_color` control until it exists, using callback.</caption>
   6066 	 * wp.customize.control( 'background_color', function( control ) { ... } );
   6067 	 *
   6068 	 * @example <caption>Get title and tagline controls when they both exist, using promise (only available when multiple IDs are present).</caption>
   6069 	 * promise = wp.customize.control( 'blogname', 'blogdescription' );
   6070 	 * promise.done( function( titleControl, taglineControl ) { ... } );
   6071 	 *
   6072 	 * @example <caption>Get title and tagline controls when they both exist, using callback.</caption>
   6073 	 * wp.customize.control( 'blogname', 'blogdescription', function( titleControl, taglineControl ) { ... } );
   6074 	 *
   6075 	 * @example <caption>Getting setting value for `background_color` control.</caption>
   6076 	 * value = wp.customize.control( 'background_color ').setting.get();
   6077 	 * value = wp.customize( 'background_color' ).get(); // Same as above, since setting ID and control ID are the same.
   6078 	 *
   6079 	 * @example <caption>Add new control for site title.</caption>
   6080 	 * wp.customize.control.add( new wp.customize.Control( 'other_blogname', {
   6081 	 *     setting: 'blogname',
   6082 	 *     type: 'text',
   6083 	 *     label: 'Site title',
   6084 	 *     section: 'other_site_identify'
   6085 	 * } ) );
   6086 	 *
   6087 	 * @example <caption>Remove control.</caption>
   6088 	 * wp.customize.control.remove( 'other_blogname' );
   6089 	 *
   6090 	 * @example <caption>Listen for control being added.</caption>
   6091 	 * wp.customize.control.bind( 'add', function( addedControl ) { ... } )
   6092 	 *
   6093 	 * @example <caption>Listen for control being removed.</caption>
   6094 	 * wp.customize.control.bind( 'removed', function( removedControl ) { ... } )
   6095 	 */
   6096 	api.control = new api.Values({ defaultConstructor: api.Control });
   6097 
   6098 	/**
   6099 	 * Callback for resolved sections.
   6100 	 *
   6101 	 * @callback wp.customize.deferredSectionsCallback
   6102 	 * @param {wp.customize.Section[]} sections Resolved sections.
   6103 	 */
   6104 
   6105 	/**
   6106 	 * Collection of all registered sections.
   6107 	 *
   6108 	 * @alias wp.customize.section
   6109 	 *
   6110 	 * @since 3.4.0
   6111 	 *
   6112 	 * @type {Function}
   6113 	 * @param {...string} ids - One or more ids for sections to obtain.
   6114 	 * @param {deferredSectionsCallback} [callback] - Function called when all supplied sections exist.
   6115 	 * @return {wp.customize.Section|undefined|jQuery.promise} Section instance or undefined (if function called with one id param),
   6116 	 *                                                         or promise resolving to requested sections.
   6117 	 *
   6118 	 * @example <caption>Loop over all registered sections.</caption>
   6119 	 * wp.customize.section.each( function( section ) { ... } )
   6120 	 *
   6121 	 * @example <caption>Getting `title_tagline` section instance.</caption>
   6122 	 * section = wp.customize.section( 'title_tagline' )
   6123 	 *
   6124 	 * @example <caption>Expand dynamically-created section when it exists.</caption>
   6125 	 * wp.customize.section( 'dynamically_created', function( section ) {
   6126 	 *     section.expand();
   6127 	 * } );
   6128 	 *
   6129 	 * @see {@link wp.customize.control} for further examples of how to interact with {@link wp.customize.Values} instances.
   6130 	 */
   6131 	api.section = new api.Values({ defaultConstructor: api.Section });
   6132 
   6133 	/**
   6134 	 * Callback for resolved panels.
   6135 	 *
   6136 	 * @callback wp.customize.deferredPanelsCallback
   6137 	 * @param {wp.customize.Panel[]} panels Resolved panels.
   6138 	 */
   6139 
   6140 	/**
   6141 	 * Collection of all registered panels.
   6142 	 *
   6143 	 * @alias wp.customize.panel
   6144 	 *
   6145 	 * @since 4.0.0
   6146 	 *
   6147 	 * @type {Function}
   6148 	 * @param {...string} ids - One or more ids for panels to obtain.
   6149 	 * @param {deferredPanelsCallback} [callback] - Function called when all supplied panels exist.
   6150 	 * @return {wp.customize.Panel|undefined|jQuery.promise} Panel instance or undefined (if function called with one id param),
   6151 	 *                                                       or promise resolving to requested panels.
   6152 	 *
   6153 	 * @example <caption>Loop over all registered panels.</caption>
   6154 	 * wp.customize.panel.each( function( panel ) { ... } )
   6155 	 *
   6156 	 * @example <caption>Getting nav_menus panel instance.</caption>
   6157 	 * panel = wp.customize.panel( 'nav_menus' );
   6158 	 *
   6159 	 * @example <caption>Expand dynamically-created panel when it exists.</caption>
   6160 	 * wp.customize.panel( 'dynamically_created', function( panel ) {
   6161 	 *     panel.expand();
   6162 	 * } );
   6163 	 *
   6164 	 * @see {@link wp.customize.control} for further examples of how to interact with {@link wp.customize.Values} instances.
   6165 	 */
   6166 	api.panel = new api.Values({ defaultConstructor: api.Panel });
   6167 
   6168 	/**
   6169 	 * Callback for resolved notifications.
   6170 	 *
   6171 	 * @callback wp.customize.deferredNotificationsCallback
   6172 	 * @param {wp.customize.Notification[]} notifications Resolved notifications.
   6173 	 */
   6174 
   6175 	/**
   6176 	 * Collection of all global notifications.
   6177 	 *
   6178 	 * @alias wp.customize.notifications
   6179 	 *
   6180 	 * @since 4.9.0
   6181 	 *
   6182 	 * @type {Function}
   6183 	 * @param {...string} codes - One or more codes for notifications to obtain.
   6184 	 * @param {deferredNotificationsCallback} [callback] - Function called when all supplied notifications exist.
   6185 	 * @return {wp.customize.Notification|undefined|jQuery.promise} Notification instance or undefined (if function called with one code param),
   6186 	 *                                                              or promise resolving to requested notifications.
   6187 	 *
   6188 	 * @example <caption>Check if existing notification</caption>
   6189 	 * exists = wp.customize.notifications.has( 'a_new_day_arrived' );
   6190 	 *
   6191 	 * @example <caption>Obtain existing notification</caption>
   6192 	 * notification = wp.customize.notifications( 'a_new_day_arrived' );
   6193 	 *
   6194 	 * @example <caption>Obtain notification that may not exist yet.</caption>
   6195 	 * wp.customize.notifications( 'a_new_day_arrived', function( notification ) { ... } );
   6196 	 *
   6197 	 * @example <caption>Add a warning notification.</caption>
   6198 	 * wp.customize.notifications.add( new wp.customize.Notification( 'midnight_almost_here', {
   6199 	 *     type: 'warning',
   6200 	 *     message: 'Midnight has almost arrived!',
   6201 	 *     dismissible: true
   6202 	 * } ) );
   6203 	 *
   6204 	 * @example <caption>Remove a notification.</caption>
   6205 	 * wp.customize.notifications.remove( 'a_new_day_arrived' );
   6206 	 *
   6207 	 * @see {@link wp.customize.control} for further examples of how to interact with {@link wp.customize.Values} instances.
   6208 	 */
   6209 	api.notifications = new api.Notifications();
   6210 
   6211 	api.PreviewFrame = api.Messenger.extend(/** @lends wp.customize.PreviewFrame.prototype */{
   6212 		sensitivity: null, // Will get set to api.settings.timeouts.previewFrameSensitivity.
   6213 
   6214 		/**
   6215 		 * An object that fetches a preview in the background of the document, which
   6216 		 * allows for seamless replacement of an existing preview.
   6217 		 *
   6218 		 * @constructs wp.customize.PreviewFrame
   6219 		 * @augments   wp.customize.Messenger
   6220 		 *
   6221 		 * @param {Object} params.container
   6222 		 * @param {Object} params.previewUrl
   6223 		 * @param {Object} params.query
   6224 		 * @param {Object} options
   6225 		 */
   6226 		initialize: function( params, options ) {
   6227 			var deferred = $.Deferred();
   6228 
   6229 			/*
   6230 			 * Make the instance of the PreviewFrame the promise object
   6231 			 * so other objects can easily interact with it.
   6232 			 */
   6233 			deferred.promise( this );
   6234 
   6235 			this.container = params.container;
   6236 
   6237 			$.extend( params, { channel: api.PreviewFrame.uuid() });
   6238 
   6239 			api.Messenger.prototype.initialize.call( this, params, options );
   6240 
   6241 			this.add( 'previewUrl', params.previewUrl );
   6242 
   6243 			this.query = $.extend( params.query || {}, { customize_messenger_channel: this.channel() });
   6244 
   6245 			this.run( deferred );
   6246 		},
   6247 
   6248 		/**
   6249 		 * Run the preview request.
   6250 		 *
   6251 		 * @param {Object} deferred jQuery Deferred object to be resolved with
   6252 		 *                          the request.
   6253 		 */
   6254 		run: function( deferred ) {
   6255 			var previewFrame = this,
   6256 				loaded = false,
   6257 				ready = false,
   6258 				readyData = null,
   6259 				hasPendingChangesetUpdate = '{}' !== previewFrame.query.customized,
   6260 				urlParser,
   6261 				params,
   6262 				form;
   6263 
   6264 			if ( previewFrame._ready ) {
   6265 				previewFrame.unbind( 'ready', previewFrame._ready );
   6266 			}
   6267 
   6268 			previewFrame._ready = function( data ) {
   6269 				ready = true;
   6270 				readyData = data;
   6271 				previewFrame.container.addClass( 'iframe-ready' );
   6272 				if ( ! data ) {
   6273 					return;
   6274 				}
   6275 
   6276 				if ( loaded ) {
   6277 					deferred.resolveWith( previewFrame, [ data ] );
   6278 				}
   6279 			};
   6280 
   6281 			previewFrame.bind( 'ready', previewFrame._ready );
   6282 
   6283 			urlParser = document.createElement( 'a' );
   6284 			urlParser.href = previewFrame.previewUrl();
   6285 
   6286 			params = _.extend(
   6287 				api.utils.parseQueryString( urlParser.search.substr( 1 ) ),
   6288 				{
   6289 					customize_changeset_uuid: previewFrame.query.customize_changeset_uuid,
   6290 					customize_theme: previewFrame.query.customize_theme,
   6291 					customize_messenger_channel: previewFrame.query.customize_messenger_channel
   6292 				}
   6293 			);
   6294 			if ( api.settings.changeset.autosaved || ! api.state( 'saved' ).get() ) {
   6295 				params.customize_autosaved = 'on';
   6296 			}
   6297 
   6298 			urlParser.search = $.param( params );
   6299 			previewFrame.iframe = $( '<iframe />', {
   6300 				title: api.l10n.previewIframeTitle,
   6301 				name: 'customize-' + previewFrame.channel()
   6302 			} );
   6303 			previewFrame.iframe.attr( 'onmousewheel', '' ); // Workaround for Safari bug. See WP Trac #38149.
   6304 			previewFrame.iframe.attr( 'sandbox', 'allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts' );
   6305 
   6306 			if ( ! hasPendingChangesetUpdate ) {
   6307 				previewFrame.iframe.attr( 'src', urlParser.href );
   6308 			} else {
   6309 				previewFrame.iframe.attr( 'data-src', urlParser.href ); // For debugging purposes.
   6310 			}
   6311 
   6312 			previewFrame.iframe.appendTo( previewFrame.container );
   6313 			previewFrame.targetWindow( previewFrame.iframe[0].contentWindow );
   6314 
   6315 			/*
   6316 			 * Submit customized data in POST request to preview frame window since
   6317 			 * there are setting value changes not yet written to changeset.
   6318 			 */
   6319 			if ( hasPendingChangesetUpdate ) {
   6320 				form = $( '<form>', {
   6321 					action: urlParser.href,
   6322 					target: previewFrame.iframe.attr( 'name' ),
   6323 					method: 'post',
   6324 					hidden: 'hidden'
   6325 				} );
   6326 				form.append( $( '<input>', {
   6327 					type: 'hidden',
   6328 					name: '_method',
   6329 					value: 'GET'
   6330 				} ) );
   6331 				_.each( previewFrame.query, function( value, key ) {
   6332 					form.append( $( '<input>', {
   6333 						type: 'hidden',
   6334 						name: key,
   6335 						value: value
   6336 					} ) );
   6337 				} );
   6338 				previewFrame.container.append( form );
   6339 				form.trigger( 'submit' );
   6340 				form.remove(); // No need to keep the form around after submitted.
   6341 			}
   6342 
   6343 			previewFrame.bind( 'iframe-loading-error', function( error ) {
   6344 				previewFrame.iframe.remove();
   6345 
   6346 				// Check if the user is not logged in.
   6347 				if ( 0 === error ) {
   6348 					previewFrame.login( deferred );
   6349 					return;
   6350 				}
   6351 
   6352 				// Check for cheaters.
   6353 				if ( -1 === error ) {
   6354 					deferred.rejectWith( previewFrame, [ 'cheatin' ] );
   6355 					return;
   6356 				}
   6357 
   6358 				deferred.rejectWith( previewFrame, [ 'request failure' ] );
   6359 			} );
   6360 
   6361 			previewFrame.iframe.one( 'load', function() {
   6362 				loaded = true;
   6363 
   6364 				if ( ready ) {
   6365 					deferred.resolveWith( previewFrame, [ readyData ] );
   6366 				} else {
   6367 					setTimeout( function() {
   6368 						deferred.rejectWith( previewFrame, [ 'ready timeout' ] );
   6369 					}, previewFrame.sensitivity );
   6370 				}
   6371 			});
   6372 		},
   6373 
   6374 		login: function( deferred ) {
   6375 			var self = this,
   6376 				reject;
   6377 
   6378 			reject = function() {
   6379 				deferred.rejectWith( self, [ 'logged out' ] );
   6380 			};
   6381 
   6382 			if ( this.triedLogin ) {
   6383 				return reject();
   6384 			}
   6385 
   6386 			// Check if we have an admin cookie.
   6387 			$.get( api.settings.url.ajax, {
   6388 				action: 'logged-in'
   6389 			}).fail( reject ).done( function( response ) {
   6390 				var iframe;
   6391 
   6392 				if ( '1' !== response ) {
   6393 					reject();
   6394 				}
   6395 
   6396 				iframe = $( '<iframe />', { 'src': self.previewUrl(), 'title': api.l10n.previewIframeTitle } ).hide();
   6397 				iframe.appendTo( self.container );
   6398 				iframe.on( 'load', function() {
   6399 					self.triedLogin = true;
   6400 
   6401 					iframe.remove();
   6402 					self.run( deferred );
   6403 				});
   6404 			});
   6405 		},
   6406 
   6407 		destroy: function() {
   6408 			api.Messenger.prototype.destroy.call( this );
   6409 
   6410 			if ( this.iframe ) {
   6411 				this.iframe.remove();
   6412 			}
   6413 
   6414 			delete this.iframe;
   6415 			delete this.targetWindow;
   6416 		}
   6417 	});
   6418 
   6419 	(function(){
   6420 		var id = 0;
   6421 		/**
   6422 		 * Return an incremented ID for a preview messenger channel.
   6423 		 *
   6424 		 * This function is named "uuid" for historical reasons, but it is a
   6425 		 * misnomer as it is not an actual UUID, and it is not universally unique.
   6426 		 * This is not to be confused with `api.settings.changeset.uuid`.
   6427 		 *
   6428 		 * @return {string}
   6429 		 */
   6430 		api.PreviewFrame.uuid = function() {
   6431 			return 'preview-' + String( id++ );
   6432 		};
   6433 	}());
   6434 
   6435 	/**
   6436 	 * Set the document title of the customizer.
   6437 	 *
   6438 	 * @alias wp.customize.setDocumentTitle
   6439 	 *
   6440 	 * @since 4.1.0
   6441 	 *
   6442 	 * @param {string} documentTitle
   6443 	 */
   6444 	api.setDocumentTitle = function ( documentTitle ) {
   6445 		var tmpl, title;
   6446 		tmpl = api.settings.documentTitleTmpl;
   6447 		title = tmpl.replace( '%s', documentTitle );
   6448 		document.title = title;
   6449 		api.trigger( 'title', title );
   6450 	};
   6451 
   6452 	api.Previewer = api.Messenger.extend(/** @lends wp.customize.Previewer.prototype */{
   6453 		refreshBuffer: null, // Will get set to api.settings.timeouts.windowRefresh.
   6454 
   6455 		/**
   6456 		 * @constructs wp.customize.Previewer
   6457 		 * @augments   wp.customize.Messenger
   6458 		 *
   6459 		 * @param {Array}  params.allowedUrls
   6460 		 * @param {string} params.container   A selector or jQuery element for the preview
   6461 		 *                                    frame to be placed.
   6462 		 * @param {string} params.form
   6463 		 * @param {string} params.previewUrl  The URL to preview.
   6464 		 * @param {Object} options
   6465 		 */
   6466 		initialize: function( params, options ) {
   6467 			var previewer = this,
   6468 				urlParser = document.createElement( 'a' );
   6469 
   6470 			$.extend( previewer, options || {} );
   6471 			previewer.deferred = {
   6472 				active: $.Deferred()
   6473 			};
   6474 
   6475 			// Debounce to prevent hammering server and then wait for any pending update requests.
   6476 			previewer.refresh = _.debounce(
   6477 				( function( originalRefresh ) {
   6478 					return function() {
   6479 						var isProcessingComplete, refreshOnceProcessingComplete;
   6480 						isProcessingComplete = function() {
   6481 							return 0 === api.state( 'processing' ).get();
   6482 						};
   6483 						if ( isProcessingComplete() ) {
   6484 							originalRefresh.call( previewer );
   6485 						} else {
   6486 							refreshOnceProcessingComplete = function() {
   6487 								if ( isProcessingComplete() ) {
   6488 									originalRefresh.call( previewer );
   6489 									api.state( 'processing' ).unbind( refreshOnceProcessingComplete );
   6490 								}
   6491 							};
   6492 							api.state( 'processing' ).bind( refreshOnceProcessingComplete );
   6493 						}
   6494 					};
   6495 				}( previewer.refresh ) ),
   6496 				previewer.refreshBuffer
   6497 			);
   6498 
   6499 			previewer.container   = api.ensure( params.container );
   6500 			previewer.allowedUrls = params.allowedUrls;
   6501 
   6502 			params.url = window.location.href;
   6503 
   6504 			api.Messenger.prototype.initialize.call( previewer, params );
   6505 
   6506 			urlParser.href = previewer.origin();
   6507 			previewer.add( 'scheme', urlParser.protocol.replace( /:$/, '' ) );
   6508 
   6509 			/*
   6510 			 * Limit the URL to internal, front-end links.
   6511 			 *
   6512 			 * If the front end and the admin are served from the same domain, load the
   6513 			 * preview over ssl if the Customizer is being loaded over ssl. This avoids
   6514 			 * insecure content warnings. This is not attempted if the admin and front end
   6515 			 * are on different domains to avoid the case where the front end doesn't have
   6516 			 * ssl certs.
   6517 			 */
   6518 
   6519 			previewer.add( 'previewUrl', params.previewUrl ).setter( function( to ) {
   6520 				var result = null, urlParser, queryParams, parsedAllowedUrl, parsedCandidateUrls = [];
   6521 				urlParser = document.createElement( 'a' );
   6522 				urlParser.href = to;
   6523 
   6524 				// Abort if URL is for admin or (static) files in wp-includes or wp-content.
   6525 				if ( /\/wp-(admin|includes|content)(\/|$)/.test( urlParser.pathname ) ) {
   6526 					return null;
   6527 				}
   6528 
   6529 				// Remove state query params.
   6530 				if ( urlParser.search.length > 1 ) {
   6531 					queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
   6532 					delete queryParams.customize_changeset_uuid;
   6533 					delete queryParams.customize_theme;
   6534 					delete queryParams.customize_messenger_channel;
   6535 					delete queryParams.customize_autosaved;
   6536 					if ( _.isEmpty( queryParams ) ) {
   6537 						urlParser.search = '';
   6538 					} else {
   6539 						urlParser.search = $.param( queryParams );
   6540 					}
   6541 				}
   6542 
   6543 				parsedCandidateUrls.push( urlParser );
   6544 
   6545 				// Prepend list with URL that matches the scheme/protocol of the iframe.
   6546 				if ( previewer.scheme.get() + ':' !== urlParser.protocol ) {
   6547 					urlParser = document.createElement( 'a' );
   6548 					urlParser.href = parsedCandidateUrls[0].href;
   6549 					urlParser.protocol = previewer.scheme.get() + ':';
   6550 					parsedCandidateUrls.unshift( urlParser );
   6551 				}
   6552 
   6553 				// Attempt to match the URL to the control frame's scheme and check if it's allowed. If not, try the original URL.
   6554 				parsedAllowedUrl = document.createElement( 'a' );
   6555 				_.find( parsedCandidateUrls, function( parsedCandidateUrl ) {
   6556 					return ! _.isUndefined( _.find( previewer.allowedUrls, function( allowedUrl ) {
   6557 						parsedAllowedUrl.href = allowedUrl;
   6558 						if ( urlParser.protocol === parsedAllowedUrl.protocol && urlParser.host === parsedAllowedUrl.host && 0 === urlParser.pathname.indexOf( parsedAllowedUrl.pathname.replace( /\/$/, '' ) ) ) {
   6559 							result = parsedCandidateUrl.href;
   6560 							return true;
   6561 						}
   6562 					} ) );
   6563 				} );
   6564 
   6565 				return result;
   6566 			});
   6567 
   6568 			previewer.bind( 'ready', previewer.ready );
   6569 
   6570 			// Start listening for keep-alive messages when iframe first loads.
   6571 			previewer.deferred.active.done( _.bind( previewer.keepPreviewAlive, previewer ) );
   6572 
   6573 			previewer.bind( 'synced', function() {
   6574 				previewer.send( 'active' );
   6575 			} );
   6576 
   6577 			// Refresh the preview when the URL is changed (but not yet).
   6578 			previewer.previewUrl.bind( previewer.refresh );
   6579 
   6580 			previewer.scroll = 0;
   6581 			previewer.bind( 'scroll', function( distance ) {
   6582 				previewer.scroll = distance;
   6583 			});
   6584 
   6585 			// Update the URL when the iframe sends a URL message, resetting scroll position. If URL is unchanged, then refresh.
   6586 			previewer.bind( 'url', function( url ) {
   6587 				var onUrlChange, urlChanged = false;
   6588 				previewer.scroll = 0;
   6589 				onUrlChange = function() {
   6590 					urlChanged = true;
   6591 				};
   6592 				previewer.previewUrl.bind( onUrlChange );
   6593 				previewer.previewUrl.set( url );
   6594 				previewer.previewUrl.unbind( onUrlChange );
   6595 				if ( ! urlChanged ) {
   6596 					previewer.refresh();
   6597 				}
   6598 			} );
   6599 
   6600 			// Update the document title when the preview changes.
   6601 			previewer.bind( 'documentTitle', function ( title ) {
   6602 				api.setDocumentTitle( title );
   6603 			} );
   6604 		},
   6605 
   6606 		/**
   6607 		 * Handle the preview receiving the ready message.
   6608 		 *
   6609 		 * @since 4.7.0
   6610 		 * @access public
   6611 		 *
   6612 		 * @param {Object} data - Data from preview.
   6613 		 * @param {string} data.currentUrl - Current URL.
   6614 		 * @param {Object} data.activePanels - Active panels.
   6615 		 * @param {Object} data.activeSections Active sections.
   6616 		 * @param {Object} data.activeControls Active controls.
   6617 		 * @return {void}
   6618 		 */
   6619 		ready: function( data ) {
   6620 			var previewer = this, synced = {}, constructs;
   6621 
   6622 			synced.settings = api.get();
   6623 			synced['settings-modified-while-loading'] = previewer.settingsModifiedWhileLoading;
   6624 			if ( 'resolved' !== previewer.deferred.active.state() || previewer.loading ) {
   6625 				synced.scroll = previewer.scroll;
   6626 			}
   6627 			synced['edit-shortcut-visibility'] = api.state( 'editShortcutVisibility' ).get();
   6628 			previewer.send( 'sync', synced );
   6629 
   6630 			// Set the previewUrl without causing the url to set the iframe.
   6631 			if ( data.currentUrl ) {
   6632 				previewer.previewUrl.unbind( previewer.refresh );
   6633 				previewer.previewUrl.set( data.currentUrl );
   6634 				previewer.previewUrl.bind( previewer.refresh );
   6635 			}
   6636 
   6637 			/*
   6638 			 * Walk over all panels, sections, and controls and set their
   6639 			 * respective active states to true if the preview explicitly
   6640 			 * indicates as such.
   6641 			 */
   6642 			constructs = {
   6643 				panel: data.activePanels,
   6644 				section: data.activeSections,
   6645 				control: data.activeControls
   6646 			};
   6647 			_( constructs ).each( function ( activeConstructs, type ) {
   6648 				api[ type ].each( function ( construct, id ) {
   6649 					var isDynamicallyCreated = _.isUndefined( api.settings[ type + 's' ][ id ] );
   6650 
   6651 					/*
   6652 					 * If the construct was created statically in PHP (not dynamically in JS)
   6653 					 * then consider a missing (undefined) value in the activeConstructs to
   6654 					 * mean it should be deactivated (since it is gone). But if it is
   6655 					 * dynamically created then only toggle activation if the value is defined,
   6656 					 * as this means that the construct was also then correspondingly
   6657 					 * created statically in PHP and the active callback is available.
   6658 					 * Otherwise, dynamically-created constructs should normally have
   6659 					 * their active states toggled in JS rather than from PHP.
   6660 					 */
   6661 					if ( ! isDynamicallyCreated || ! _.isUndefined( activeConstructs[ id ] ) ) {
   6662 						if ( activeConstructs[ id ] ) {
   6663 							construct.activate();
   6664 						} else {
   6665 							construct.deactivate();
   6666 						}
   6667 					}
   6668 				} );
   6669 			} );
   6670 
   6671 			if ( data.settingValidities ) {
   6672 				api._handleSettingValidities( {
   6673 					settingValidities: data.settingValidities,
   6674 					focusInvalidControl: false
   6675 				} );
   6676 			}
   6677 		},
   6678 
   6679 		/**
   6680 		 * Keep the preview alive by listening for ready and keep-alive messages.
   6681 		 *
   6682 		 * If a message is not received in the allotted time then the iframe will be set back to the last known valid URL.
   6683 		 *
   6684 		 * @since 4.7.0
   6685 		 * @access public
   6686 		 *
   6687 		 * @return {void}
   6688 		 */
   6689 		keepPreviewAlive: function keepPreviewAlive() {
   6690 			var previewer = this, keepAliveTick, timeoutId, handleMissingKeepAlive, scheduleKeepAliveCheck;
   6691 
   6692 			/**
   6693 			 * Schedule a preview keep-alive check.
   6694 			 *
   6695 			 * Note that if a page load takes longer than keepAliveCheck milliseconds,
   6696 			 * the keep-alive messages will still be getting sent from the previous
   6697 			 * URL.
   6698 			 */
   6699 			scheduleKeepAliveCheck = function() {
   6700 				timeoutId = setTimeout( handleMissingKeepAlive, api.settings.timeouts.keepAliveCheck );
   6701 			};
   6702 
   6703 			/**
   6704 			 * Set the previewerAlive state to true when receiving a message from the preview.
   6705 			 */
   6706 			keepAliveTick = function() {
   6707 				api.state( 'previewerAlive' ).set( true );
   6708 				clearTimeout( timeoutId );
   6709 				scheduleKeepAliveCheck();
   6710 			};
   6711 
   6712 			/**
   6713 			 * Set the previewerAlive state to false if keepAliveCheck milliseconds have transpired without a message.
   6714 			 *
   6715 			 * This is most likely to happen in the case of a connectivity error, or if the theme causes the browser
   6716 			 * to navigate to a non-allowed URL. Setting this state to false will force settings with a postMessage
   6717 			 * transport to use refresh instead, causing the preview frame also to be replaced with the current
   6718 			 * allowed preview URL.
   6719 			 */
   6720 			handleMissingKeepAlive = function() {
   6721 				api.state( 'previewerAlive' ).set( false );
   6722 			};
   6723 			scheduleKeepAliveCheck();
   6724 
   6725 			previewer.bind( 'ready', keepAliveTick );
   6726 			previewer.bind( 'keep-alive', keepAliveTick );
   6727 		},
   6728 
   6729 		/**
   6730 		 * Query string data sent with each preview request.
   6731 		 *
   6732 		 * @abstract
   6733 		 */
   6734 		query: function() {},
   6735 
   6736 		abort: function() {
   6737 			if ( this.loading ) {
   6738 				this.loading.destroy();
   6739 				delete this.loading;
   6740 			}
   6741 		},
   6742 
   6743 		/**
   6744 		 * Refresh the preview seamlessly.
   6745 		 *
   6746 		 * @since 3.4.0
   6747 		 * @access public
   6748 		 *
   6749 		 * @return {void}
   6750 		 */
   6751 		refresh: function() {
   6752 			var previewer = this, onSettingChange;
   6753 
   6754 			// Display loading indicator.
   6755 			previewer.send( 'loading-initiated' );
   6756 
   6757 			previewer.abort();
   6758 
   6759 			previewer.loading = new api.PreviewFrame({
   6760 				url:        previewer.url(),
   6761 				previewUrl: previewer.previewUrl(),
   6762 				query:      previewer.query( { excludeCustomizedSaved: true } ) || {},
   6763 				container:  previewer.container
   6764 			});
   6765 
   6766 			previewer.settingsModifiedWhileLoading = {};
   6767 			onSettingChange = function( setting ) {
   6768 				previewer.settingsModifiedWhileLoading[ setting.id ] = true;
   6769 			};
   6770 			api.bind( 'change', onSettingChange );
   6771 			previewer.loading.always( function() {
   6772 				api.unbind( 'change', onSettingChange );
   6773 			} );
   6774 
   6775 			previewer.loading.done( function( readyData ) {
   6776 				var loadingFrame = this, onceSynced;
   6777 
   6778 				previewer.preview = loadingFrame;
   6779 				previewer.targetWindow( loadingFrame.targetWindow() );
   6780 				previewer.channel( loadingFrame.channel() );
   6781 
   6782 				onceSynced = function() {
   6783 					loadingFrame.unbind( 'synced', onceSynced );
   6784 					if ( previewer._previousPreview ) {
   6785 						previewer._previousPreview.destroy();
   6786 					}
   6787 					previewer._previousPreview = previewer.preview;
   6788 					previewer.deferred.active.resolve();
   6789 					delete previewer.loading;
   6790 				};
   6791 				loadingFrame.bind( 'synced', onceSynced );
   6792 
   6793 				// This event will be received directly by the previewer in normal navigation; this is only needed for seamless refresh.
   6794 				previewer.trigger( 'ready', readyData );
   6795 			});
   6796 
   6797 			previewer.loading.fail( function( reason ) {
   6798 				previewer.send( 'loading-failed' );
   6799 
   6800 				if ( 'logged out' === reason ) {
   6801 					if ( previewer.preview ) {
   6802 						previewer.preview.destroy();
   6803 						delete previewer.preview;
   6804 					}
   6805 
   6806 					previewer.login().done( previewer.refresh );
   6807 				}
   6808 
   6809 				if ( 'cheatin' === reason ) {
   6810 					previewer.cheatin();
   6811 				}
   6812 			});
   6813 		},
   6814 
   6815 		login: function() {
   6816 			var previewer = this,
   6817 				deferred, messenger, iframe;
   6818 
   6819 			if ( this._login ) {
   6820 				return this._login;
   6821 			}
   6822 
   6823 			deferred = $.Deferred();
   6824 			this._login = deferred.promise();
   6825 
   6826 			messenger = new api.Messenger({
   6827 				channel: 'login',
   6828 				url:     api.settings.url.login
   6829 			});
   6830 
   6831 			iframe = $( '<iframe />', { 'src': api.settings.url.login, 'title': api.l10n.loginIframeTitle } ).appendTo( this.container );
   6832 
   6833 			messenger.targetWindow( iframe[0].contentWindow );
   6834 
   6835 			messenger.bind( 'login', function () {
   6836 				var refreshNonces = previewer.refreshNonces();
   6837 
   6838 				refreshNonces.always( function() {
   6839 					iframe.remove();
   6840 					messenger.destroy();
   6841 					delete previewer._login;
   6842 				});
   6843 
   6844 				refreshNonces.done( function() {
   6845 					deferred.resolve();
   6846 				});
   6847 
   6848 				refreshNonces.fail( function() {
   6849 					previewer.cheatin();
   6850 					deferred.reject();
   6851 				});
   6852 			});
   6853 
   6854 			return this._login;
   6855 		},
   6856 
   6857 		cheatin: function() {
   6858 			$( document.body ).empty().addClass( 'cheatin' ).append(
   6859 				'<h1>' + api.l10n.notAllowedHeading + '</h1>' +
   6860 				'<p>' + api.l10n.notAllowed + '</p>'
   6861 			);
   6862 		},
   6863 
   6864 		refreshNonces: function() {
   6865 			var request, deferred = $.Deferred();
   6866 
   6867 			deferred.promise();
   6868 
   6869 			request = wp.ajax.post( 'customize_refresh_nonces', {
   6870 				wp_customize: 'on',
   6871 				customize_theme: api.settings.theme.stylesheet
   6872 			});
   6873 
   6874 			request.done( function( response ) {
   6875 				api.trigger( 'nonce-refresh', response );
   6876 				deferred.resolve();
   6877 			});
   6878 
   6879 			request.fail( function() {
   6880 				deferred.reject();
   6881 			});
   6882 
   6883 			return deferred;
   6884 		}
   6885 	});
   6886 
   6887 	api.settingConstructor = {};
   6888 	api.controlConstructor = {
   6889 		color:               api.ColorControl,
   6890 		media:               api.MediaControl,
   6891 		upload:              api.UploadControl,
   6892 		image:               api.ImageControl,
   6893 		cropped_image:       api.CroppedImageControl,
   6894 		site_icon:           api.SiteIconControl,
   6895 		header:              api.HeaderControl,
   6896 		background:          api.BackgroundControl,
   6897 		background_position: api.BackgroundPositionControl,
   6898 		theme:               api.ThemeControl,
   6899 		date_time:           api.DateTimeControl,
   6900 		code_editor:         api.CodeEditorControl
   6901 	};
   6902 	api.panelConstructor = {
   6903 		themes: api.ThemesPanel
   6904 	};
   6905 	api.sectionConstructor = {
   6906 		themes: api.ThemesSection,
   6907 		outer: api.OuterSection
   6908 	};
   6909 
   6910 	/**
   6911 	 * Handle setting_validities in an error response for the customize-save request.
   6912 	 *
   6913 	 * Add notifications to the settings and focus on the first control that has an invalid setting.
   6914 	 *
   6915 	 * @alias wp.customize._handleSettingValidities
   6916 	 *
   6917 	 * @since 4.6.0
   6918 	 * @private
   6919 	 *
   6920 	 * @param {Object}  args
   6921 	 * @param {Object}  args.settingValidities
   6922 	 * @param {boolean} [args.focusInvalidControl=false]
   6923 	 * @return {void}
   6924 	 */
   6925 	api._handleSettingValidities = function handleSettingValidities( args ) {
   6926 		var invalidSettingControls, invalidSettings = [], wasFocused = false;
   6927 
   6928 		// Find the controls that correspond to each invalid setting.
   6929 		_.each( args.settingValidities, function( validity, settingId ) {
   6930 			var setting = api( settingId );
   6931 			if ( setting ) {
   6932 
   6933 				// Add notifications for invalidities.
   6934 				if ( _.isObject( validity ) ) {
   6935 					_.each( validity, function( params, code ) {
   6936 						var notification, existingNotification, needsReplacement = false;
   6937 						notification = new api.Notification( code, _.extend( { fromServer: true }, params ) );
   6938 
   6939 						// Remove existing notification if already exists for code but differs in parameters.
   6940 						existingNotification = setting.notifications( notification.code );
   6941 						if ( existingNotification ) {
   6942 							needsReplacement = notification.type !== existingNotification.type || notification.message !== existingNotification.message || ! _.isEqual( notification.data, existingNotification.data );
   6943 						}
   6944 						if ( needsReplacement ) {
   6945 							setting.notifications.remove( code );
   6946 						}
   6947 
   6948 						if ( ! setting.notifications.has( notification.code ) ) {
   6949 							setting.notifications.add( notification );
   6950 						}
   6951 						invalidSettings.push( setting.id );
   6952 					} );
   6953 				}
   6954 
   6955 				// Remove notification errors that are no longer valid.
   6956 				setting.notifications.each( function( notification ) {
   6957 					if ( notification.fromServer && 'error' === notification.type && ( true === validity || ! validity[ notification.code ] ) ) {
   6958 						setting.notifications.remove( notification.code );
   6959 					}
   6960 				} );
   6961 			}
   6962 		} );
   6963 
   6964 		if ( args.focusInvalidControl ) {
   6965 			invalidSettingControls = api.findControlsForSettings( invalidSettings );
   6966 
   6967 			// Focus on the first control that is inside of an expanded section (one that is visible).
   6968 			_( _.values( invalidSettingControls ) ).find( function( controls ) {
   6969 				return _( controls ).find( function( control ) {
   6970 					var isExpanded = control.section() && api.section.has( control.section() ) && api.section( control.section() ).expanded();
   6971 					if ( isExpanded && control.expanded ) {
   6972 						isExpanded = control.expanded();
   6973 					}
   6974 					if ( isExpanded ) {
   6975 						control.focus();
   6976 						wasFocused = true;
   6977 					}
   6978 					return wasFocused;
   6979 				} );
   6980 			} );
   6981 
   6982 			// Focus on the first invalid control.
   6983 			if ( ! wasFocused && ! _.isEmpty( invalidSettingControls ) ) {
   6984 				_.values( invalidSettingControls )[0][0].focus();
   6985 			}
   6986 		}
   6987 	};
   6988 
   6989 	/**
   6990 	 * Find all controls associated with the given settings.
   6991 	 *
   6992 	 * @alias wp.customize.findControlsForSettings
   6993 	 *
   6994 	 * @since 4.6.0
   6995 	 * @param {string[]} settingIds Setting IDs.
   6996 	 * @return {Object<string, wp.customize.Control>} Mapping setting ids to arrays of controls.
   6997 	 */
   6998 	api.findControlsForSettings = function findControlsForSettings( settingIds ) {
   6999 		var controls = {}, settingControls;
   7000 		_.each( _.unique( settingIds ), function( settingId ) {
   7001 			var setting = api( settingId );
   7002 			if ( setting ) {
   7003 				settingControls = setting.findControls();
   7004 				if ( settingControls && settingControls.length > 0 ) {
   7005 					controls[ settingId ] = settingControls;
   7006 				}
   7007 			}
   7008 		} );
   7009 		return controls;
   7010 	};
   7011 
   7012 	/**
   7013 	 * Sort panels, sections, controls by priorities. Hide empty sections and panels.
   7014 	 *
   7015 	 * @alias wp.customize.reflowPaneContents
   7016 	 *
   7017 	 * @since 4.1.0
   7018 	 */
   7019 	api.reflowPaneContents = _.bind( function () {
   7020 
   7021 		var appendContainer, activeElement, rootHeadContainers, rootNodes = [], wasReflowed = false;
   7022 
   7023 		if ( document.activeElement ) {
   7024 			activeElement = $( document.activeElement );
   7025 		}
   7026 
   7027 		// Sort the sections within each panel.
   7028 		api.panel.each( function ( panel ) {
   7029 			if ( 'themes' === panel.id ) {
   7030 				return; // Don't reflow theme sections, as doing so moves them after the themes container.
   7031 			}
   7032 
   7033 			var sections = panel.sections(),
   7034 				sectionHeadContainers = _.pluck( sections, 'headContainer' );
   7035 			rootNodes.push( panel );
   7036 			appendContainer = ( panel.contentContainer.is( 'ul' ) ) ? panel.contentContainer : panel.contentContainer.find( 'ul:first' );
   7037 			if ( ! api.utils.areElementListsEqual( sectionHeadContainers, appendContainer.children( '[id]' ) ) ) {
   7038 				_( sections ).each( function ( section ) {
   7039 					appendContainer.append( section.headContainer );
   7040 				} );
   7041 				wasReflowed = true;
   7042 			}
   7043 		} );
   7044 
   7045 		// Sort the controls within each section.
   7046 		api.section.each( function ( section ) {
   7047 			var controls = section.controls(),
   7048 				controlContainers = _.pluck( controls, 'container' );
   7049 			if ( ! section.panel() ) {
   7050 				rootNodes.push( section );
   7051 			}
   7052 			appendContainer = ( section.contentContainer.is( 'ul' ) ) ? section.contentContainer : section.contentContainer.find( 'ul:first' );
   7053 			if ( ! api.utils.areElementListsEqual( controlContainers, appendContainer.children( '[id]' ) ) ) {
   7054 				_( controls ).each( function ( control ) {
   7055 					appendContainer.append( control.container );
   7056 				} );
   7057 				wasReflowed = true;
   7058 			}
   7059 		} );
   7060 
   7061 		// Sort the root panels and sections.
   7062 		rootNodes.sort( api.utils.prioritySort );
   7063 		rootHeadContainers = _.pluck( rootNodes, 'headContainer' );
   7064 		appendContainer = $( '#customize-theme-controls .customize-pane-parent' ); // @todo This should be defined elsewhere, and to be configurable.
   7065 		if ( ! api.utils.areElementListsEqual( rootHeadContainers, appendContainer.children() ) ) {
   7066 			_( rootNodes ).each( function ( rootNode ) {
   7067 				appendContainer.append( rootNode.headContainer );
   7068 			} );
   7069 			wasReflowed = true;
   7070 		}
   7071 
   7072 		// Now re-trigger the active Value callbacks so that the panels and sections can decide whether they can be rendered.
   7073 		api.panel.each( function ( panel ) {
   7074 			var value = panel.active();
   7075 			panel.active.callbacks.fireWith( panel.active, [ value, value ] );
   7076 		} );
   7077 		api.section.each( function ( section ) {
   7078 			var value = section.active();
   7079 			section.active.callbacks.fireWith( section.active, [ value, value ] );
   7080 		} );
   7081 
   7082 		// Restore focus if there was a reflow and there was an active (focused) element.
   7083 		if ( wasReflowed && activeElement ) {
   7084 			activeElement.trigger( 'focus' );
   7085 		}
   7086 		api.trigger( 'pane-contents-reflowed' );
   7087 	}, api );
   7088 
   7089 	// Define state values.
   7090 	api.state = new api.Values();
   7091 	_.each( [
   7092 		'saved',
   7093 		'saving',
   7094 		'trashing',
   7095 		'activated',
   7096 		'processing',
   7097 		'paneVisible',
   7098 		'expandedPanel',
   7099 		'expandedSection',
   7100 		'changesetDate',
   7101 		'selectedChangesetDate',
   7102 		'changesetStatus',
   7103 		'selectedChangesetStatus',
   7104 		'remainingTimeToPublish',
   7105 		'previewerAlive',
   7106 		'editShortcutVisibility',
   7107 		'changesetLocked',
   7108 		'previewedDevice'
   7109 	], function( name ) {
   7110 		api.state.create( name );
   7111 	});
   7112 
   7113 	$( function() {
   7114 		api.settings = window._wpCustomizeSettings;
   7115 		api.l10n = window._wpCustomizeControlsL10n;
   7116 
   7117 		// Check if we can run the Customizer.
   7118 		if ( ! api.settings ) {
   7119 			return;
   7120 		}
   7121 
   7122 		// Bail if any incompatibilities are found.
   7123 		if ( ! $.support.postMessage || ( ! $.support.cors && api.settings.isCrossDomain ) ) {
   7124 			return;
   7125 		}
   7126 
   7127 		if ( null === api.PreviewFrame.prototype.sensitivity ) {
   7128 			api.PreviewFrame.prototype.sensitivity = api.settings.timeouts.previewFrameSensitivity;
   7129 		}
   7130 		if ( null === api.Previewer.prototype.refreshBuffer ) {
   7131 			api.Previewer.prototype.refreshBuffer = api.settings.timeouts.windowRefresh;
   7132 		}
   7133 
   7134 		var parent,
   7135 			body = $( document.body ),
   7136 			overlay = body.children( '.wp-full-overlay' ),
   7137 			title = $( '#customize-info .panel-title.site-title' ),
   7138 			closeBtn = $( '.customize-controls-close' ),
   7139 			saveBtn = $( '#save' ),
   7140 			btnWrapper = $( '#customize-save-button-wrapper' ),
   7141 			publishSettingsBtn = $( '#publish-settings' ),
   7142 			footerActions = $( '#customize-footer-actions' );
   7143 
   7144 		// Add publish settings section in JS instead of PHP since the Customizer depends on it to function.
   7145 		api.bind( 'ready', function() {
   7146 			api.section.add( new api.OuterSection( 'publish_settings', {
   7147 				title: api.l10n.publishSettings,
   7148 				priority: 0,
   7149 				active: api.settings.theme.active
   7150 			} ) );
   7151 		} );
   7152 
   7153 		// Set up publish settings section and its controls.
   7154 		api.section( 'publish_settings', function( section ) {
   7155 			var updateButtonsState, trashControl, updateSectionActive, isSectionActive, statusControl, dateControl, toggleDateControl, publishWhenTime, pollInterval, updateTimeArrivedPoller, cancelScheduleButtonReminder, timeArrivedPollingInterval = 1000;
   7156 
   7157 			trashControl = new api.Control( 'trash_changeset', {
   7158 				type: 'button',
   7159 				section: section.id,
   7160 				priority: 30,
   7161 				input_attrs: {
   7162 					'class': 'button-link button-link-delete',
   7163 					value: api.l10n.discardChanges
   7164 				}
   7165 			} );
   7166 			api.control.add( trashControl );
   7167 			trashControl.deferred.embedded.done( function() {
   7168 				trashControl.container.find( '.button-link' ).on( 'click', function() {
   7169 					if ( confirm( api.l10n.trashConfirm ) ) {
   7170 						wp.customize.previewer.trash();
   7171 					}
   7172 				} );
   7173 			} );
   7174 
   7175 			api.control.add( new api.PreviewLinkControl( 'changeset_preview_link', {
   7176 				section: section.id,
   7177 				priority: 100
   7178 			} ) );
   7179 
   7180 			/**
   7181 			 * Return whether the pubish settings section should be active.
   7182 			 *
   7183 			 * @return {boolean} Is section active.
   7184 			 */
   7185 			isSectionActive = function() {
   7186 				if ( ! api.state( 'activated' ).get() ) {
   7187 					return false;
   7188 				}
   7189 				if ( api.state( 'trashing' ).get() || 'trash' === api.state( 'changesetStatus' ).get() ) {
   7190 					return false;
   7191 				}
   7192 				if ( '' === api.state( 'changesetStatus' ).get() && api.state( 'saved' ).get() ) {
   7193 					return false;
   7194 				}
   7195 				return true;
   7196 			};
   7197 
   7198 			// Make sure publish settings are not available while the theme is not active and the customizer is in a published state.
   7199 			section.active.validate = isSectionActive;
   7200 			updateSectionActive = function() {
   7201 				section.active.set( isSectionActive() );
   7202 			};
   7203 			api.state( 'activated' ).bind( updateSectionActive );
   7204 			api.state( 'trashing' ).bind( updateSectionActive );
   7205 			api.state( 'saved' ).bind( updateSectionActive );
   7206 			api.state( 'changesetStatus' ).bind( updateSectionActive );
   7207 			updateSectionActive();
   7208 
   7209 			// Bind visibility of the publish settings button to whether the section is active.
   7210 			updateButtonsState = function() {
   7211 				publishSettingsBtn.toggle( section.active.get() );
   7212 				saveBtn.toggleClass( 'has-next-sibling', section.active.get() );
   7213 			};
   7214 			updateButtonsState();
   7215 			section.active.bind( updateButtonsState );
   7216 
   7217 			function highlightScheduleButton() {
   7218 				if ( ! cancelScheduleButtonReminder ) {
   7219 					cancelScheduleButtonReminder = api.utils.highlightButton( btnWrapper, {
   7220 						delay: 1000,
   7221 
   7222 						/*
   7223 						 * Only abort the reminder when the save button is focused.
   7224 						 * If the user clicks the settings button to toggle the
   7225 						 * settings closed, we'll still remind them.
   7226 						 */
   7227 						focusTarget: saveBtn
   7228 					} );
   7229 				}
   7230 			}
   7231 			function cancelHighlightScheduleButton() {
   7232 				if ( cancelScheduleButtonReminder ) {
   7233 					cancelScheduleButtonReminder();
   7234 					cancelScheduleButtonReminder = null;
   7235 				}
   7236 			}
   7237 			api.state( 'selectedChangesetStatus' ).bind( cancelHighlightScheduleButton );
   7238 
   7239 			section.contentContainer.find( '.customize-action' ).text( api.l10n.updating );
   7240 			section.contentContainer.find( '.customize-section-back' ).removeAttr( 'tabindex' );
   7241 			publishSettingsBtn.prop( 'disabled', false );
   7242 
   7243 			publishSettingsBtn.on( 'click', function( event ) {
   7244 				event.preventDefault();
   7245 				section.expanded.set( ! section.expanded.get() );
   7246 			} );
   7247 
   7248 			section.expanded.bind( function( isExpanded ) {
   7249 				var defaultChangesetStatus;
   7250 				publishSettingsBtn.attr( 'aria-expanded', String( isExpanded ) );
   7251 				publishSettingsBtn.toggleClass( 'active', isExpanded );
   7252 
   7253 				if ( isExpanded ) {
   7254 					cancelHighlightScheduleButton();
   7255 					return;
   7256 				}
   7257 
   7258 				defaultChangesetStatus = api.state( 'changesetStatus' ).get();
   7259 				if ( '' === defaultChangesetStatus || 'auto-draft' === defaultChangesetStatus ) {
   7260 					defaultChangesetStatus = 'publish';
   7261 				}
   7262 
   7263 				if ( api.state( 'selectedChangesetStatus' ).get() !== defaultChangesetStatus ) {
   7264 					highlightScheduleButton();
   7265 				} else if ( 'future' === api.state( 'selectedChangesetStatus' ).get() && api.state( 'selectedChangesetDate' ).get() !== api.state( 'changesetDate' ).get() ) {
   7266 					highlightScheduleButton();
   7267 				}
   7268 			} );
   7269 
   7270 			statusControl = new api.Control( 'changeset_status', {
   7271 				priority: 10,
   7272 				type: 'radio',
   7273 				section: 'publish_settings',
   7274 				setting: api.state( 'selectedChangesetStatus' ),
   7275 				templateId: 'customize-selected-changeset-status-control',
   7276 				label: api.l10n.action,
   7277 				choices: api.settings.changeset.statusChoices
   7278 			} );
   7279 			api.control.add( statusControl );
   7280 
   7281 			dateControl = new api.DateTimeControl( 'changeset_scheduled_date', {
   7282 				priority: 20,
   7283 				section: 'publish_settings',
   7284 				setting: api.state( 'selectedChangesetDate' ),
   7285 				minYear: ( new Date() ).getFullYear(),
   7286 				allowPastDate: false,
   7287 				includeTime: true,
   7288 				twelveHourFormat: /a/i.test( api.settings.timeFormat ),
   7289 				description: api.l10n.scheduleDescription
   7290 			} );
   7291 			dateControl.notifications.alt = true;
   7292 			api.control.add( dateControl );
   7293 
   7294 			publishWhenTime = function() {
   7295 				api.state( 'selectedChangesetStatus' ).set( 'publish' );
   7296 				api.previewer.save();
   7297 			};
   7298 
   7299 			// Start countdown for when the dateTime arrives, or clear interval when it is .
   7300 			updateTimeArrivedPoller = function() {
   7301 				var shouldPoll = (
   7302 					'future' === api.state( 'changesetStatus' ).get() &&
   7303 					'future' === api.state( 'selectedChangesetStatus' ).get() &&
   7304 					api.state( 'changesetDate' ).get() &&
   7305 					api.state( 'selectedChangesetDate' ).get() === api.state( 'changesetDate' ).get() &&
   7306 					api.utils.getRemainingTime( api.state( 'changesetDate' ).get() ) >= 0
   7307 				);
   7308 
   7309 				if ( shouldPoll && ! pollInterval ) {
   7310 					pollInterval = setInterval( function() {
   7311 						var remainingTime = api.utils.getRemainingTime( api.state( 'changesetDate' ).get() );
   7312 						api.state( 'remainingTimeToPublish' ).set( remainingTime );
   7313 						if ( remainingTime <= 0 ) {
   7314 							clearInterval( pollInterval );
   7315 							pollInterval = 0;
   7316 							publishWhenTime();
   7317 						}
   7318 					}, timeArrivedPollingInterval );
   7319 				} else if ( ! shouldPoll && pollInterval ) {
   7320 					clearInterval( pollInterval );
   7321 					pollInterval = 0;
   7322 				}
   7323 			};
   7324 
   7325 			api.state( 'changesetDate' ).bind( updateTimeArrivedPoller );
   7326 			api.state( 'selectedChangesetDate' ).bind( updateTimeArrivedPoller );
   7327 			api.state( 'changesetStatus' ).bind( updateTimeArrivedPoller );
   7328 			api.state( 'selectedChangesetStatus' ).bind( updateTimeArrivedPoller );
   7329 			updateTimeArrivedPoller();
   7330 
   7331 			// Ensure dateControl only appears when selected status is future.
   7332 			dateControl.active.validate = function() {
   7333 				return 'future' === api.state( 'selectedChangesetStatus' ).get();
   7334 			};
   7335 			toggleDateControl = function( value ) {
   7336 				dateControl.active.set( 'future' === value );
   7337 			};
   7338 			toggleDateControl( api.state( 'selectedChangesetStatus' ).get() );
   7339 			api.state( 'selectedChangesetStatus' ).bind( toggleDateControl );
   7340 
   7341 			// Show notification on date control when status is future but it isn't a future date.
   7342 			api.state( 'saving' ).bind( function( isSaving ) {
   7343 				if ( isSaving && 'future' === api.state( 'selectedChangesetStatus' ).get() ) {
   7344 					dateControl.toggleFutureDateNotification( ! dateControl.isFutureDate() );
   7345 				}
   7346 			} );
   7347 		} );
   7348 
   7349 		// Prevent the form from saving when enter is pressed on an input or select element.
   7350 		$('#customize-controls').on( 'keydown', function( e ) {
   7351 			var isEnter = ( 13 === e.which ),
   7352 				$el = $( e.target );
   7353 
   7354 			if ( isEnter && ( $el.is( 'input:not([type=button])' ) || $el.is( 'select' ) ) ) {
   7355 				e.preventDefault();
   7356 			}
   7357 		});
   7358 
   7359 		// Expand/Collapse the main customizer customize info.
   7360 		$( '.customize-info' ).find( '> .accordion-section-title .customize-help-toggle' ).on( 'click', function() {
   7361 			var section = $( this ).closest( '.accordion-section' ),
   7362 				content = section.find( '.customize-panel-description:first' );
   7363 
   7364 			if ( section.hasClass( 'cannot-expand' ) ) {
   7365 				return;
   7366 			}
   7367 
   7368 			if ( section.hasClass( 'open' ) ) {
   7369 				section.toggleClass( 'open' );
   7370 				content.slideUp( api.Panel.prototype.defaultExpandedArguments.duration, function() {
   7371 					content.trigger( 'toggled' );
   7372 				} );
   7373 				$( this ).attr( 'aria-expanded', false );
   7374 			} else {
   7375 				content.slideDown( api.Panel.prototype.defaultExpandedArguments.duration, function() {
   7376 					content.trigger( 'toggled' );
   7377 				} );
   7378 				section.toggleClass( 'open' );
   7379 				$( this ).attr( 'aria-expanded', true );
   7380 			}
   7381 		});
   7382 
   7383 		/**
   7384 		 * Initialize Previewer
   7385 		 *
   7386 		 * @alias wp.customize.previewer
   7387 		 */
   7388 		api.previewer = new api.Previewer({
   7389 			container:   '#customize-preview',
   7390 			form:        '#customize-controls',
   7391 			previewUrl:  api.settings.url.preview,
   7392 			allowedUrls: api.settings.url.allowed
   7393 		},/** @lends wp.customize.previewer */{
   7394 
   7395 			nonce: api.settings.nonce,
   7396 
   7397 			/**
   7398 			 * Build the query to send along with the Preview request.
   7399 			 *
   7400 			 * @since 3.4.0
   7401 			 * @since 4.7.0 Added options param.
   7402 			 * @access public
   7403 			 *
   7404 			 * @param {Object}  [options] Options.
   7405 			 * @param {boolean} [options.excludeCustomizedSaved=false] Exclude saved settings in customized response (values pending writing to changeset).
   7406 			 * @return {Object} Query vars.
   7407 			 */
   7408 			query: function( options ) {
   7409 				var queryVars = {
   7410 					wp_customize: 'on',
   7411 					customize_theme: api.settings.theme.stylesheet,
   7412 					nonce: this.nonce.preview,
   7413 					customize_changeset_uuid: api.settings.changeset.uuid
   7414 				};
   7415 				if ( api.settings.changeset.autosaved || ! api.state( 'saved' ).get() ) {
   7416 					queryVars.customize_autosaved = 'on';
   7417 				}
   7418 
   7419 				/*
   7420 				 * Exclude customized data if requested especially for calls to requestChangesetUpdate.
   7421 				 * Changeset updates are differential and so it is a performance waste to send all of
   7422 				 * the dirty settings with each update.
   7423 				 */
   7424 				queryVars.customized = JSON.stringify( api.dirtyValues( {
   7425 					unsaved: options && options.excludeCustomizedSaved
   7426 				} ) );
   7427 
   7428 				return queryVars;
   7429 			},
   7430 
   7431 			/**
   7432 			 * Save (and publish) the customizer changeset.
   7433 			 *
   7434 			 * Updates to the changeset are transactional. If any of the settings
   7435 			 * are invalid then none of them will be written into the changeset.
   7436 			 * A revision will be made for the changeset post if revisions support
   7437 			 * has been added to the post type.
   7438 			 *
   7439 			 * @since 3.4.0
   7440 			 * @since 4.7.0 Added args param and return value.
   7441 			 *
   7442 			 * @param {Object} [args] Args.
   7443 			 * @param {string} [args.status=publish] Status.
   7444 			 * @param {string} [args.date] Date, in local time in MySQL format.
   7445 			 * @param {string} [args.title] Title
   7446 			 * @return {jQuery.promise} Promise.
   7447 			 */
   7448 			save: function( args ) {
   7449 				var previewer = this,
   7450 					deferred = $.Deferred(),
   7451 					changesetStatus = api.state( 'selectedChangesetStatus' ).get(),
   7452 					selectedChangesetDate = api.state( 'selectedChangesetDate' ).get(),
   7453 					processing = api.state( 'processing' ),
   7454 					submitWhenDoneProcessing,
   7455 					submit,
   7456 					modifiedWhileSaving = {},
   7457 					invalidSettings = [],
   7458 					invalidControls = [],
   7459 					invalidSettingLessControls = [];
   7460 
   7461 				if ( args && args.status ) {
   7462 					changesetStatus = args.status;
   7463 				}
   7464 
   7465 				if ( api.state( 'saving' ).get() ) {
   7466 					deferred.reject( 'already_saving' );
   7467 					deferred.promise();
   7468 				}
   7469 
   7470 				api.state( 'saving' ).set( true );
   7471 
   7472 				function captureSettingModifiedDuringSave( setting ) {
   7473 					modifiedWhileSaving[ setting.id ] = true;
   7474 				}
   7475 
   7476 				submit = function () {
   7477 					var request, query, settingInvalidities = {}, latestRevision = api._latestRevision, errorCode = 'client_side_error';
   7478 
   7479 					api.bind( 'change', captureSettingModifiedDuringSave );
   7480 					api.notifications.remove( errorCode );
   7481 
   7482 					/*
   7483 					 * Block saving if there are any settings that are marked as
   7484 					 * invalid from the client (not from the server). Focus on
   7485 					 * the control.
   7486 					 */
   7487 					api.each( function( setting ) {
   7488 						setting.notifications.each( function( notification ) {
   7489 							if ( 'error' === notification.type && ! notification.fromServer ) {
   7490 								invalidSettings.push( setting.id );
   7491 								if ( ! settingInvalidities[ setting.id ] ) {
   7492 									settingInvalidities[ setting.id ] = {};
   7493 								}
   7494 								settingInvalidities[ setting.id ][ notification.code ] = notification;
   7495 							}
   7496 						} );
   7497 					} );
   7498 
   7499 					// Find all invalid setting less controls with notification type error.
   7500 					api.control.each( function( control ) {
   7501 						if ( ! control.setting || ! control.setting.id && control.active.get() ) {
   7502 							control.notifications.each( function( notification ) {
   7503 							    if ( 'error' === notification.type ) {
   7504 								    invalidSettingLessControls.push( [ control ] );
   7505 							    }
   7506 							} );
   7507 						}
   7508 					} );
   7509 
   7510 					invalidControls = _.union( invalidSettingLessControls, _.values( api.findControlsForSettings( invalidSettings ) ) );
   7511 					if ( ! _.isEmpty( invalidControls ) ) {
   7512 
   7513 						invalidControls[0][0].focus();
   7514 						api.unbind( 'change', captureSettingModifiedDuringSave );
   7515 
   7516 						if ( invalidSettings.length ) {
   7517 							api.notifications.add( new api.Notification( errorCode, {
   7518 								message: ( 1 === invalidSettings.length ? api.l10n.saveBlockedError.singular : api.l10n.saveBlockedError.plural ).replace( /%s/g, String( invalidSettings.length ) ),
   7519 								type: 'error',
   7520 								dismissible: true,
   7521 								saveFailure: true
   7522 							} ) );
   7523 						}
   7524 
   7525 						deferred.rejectWith( previewer, [
   7526 							{ setting_invalidities: settingInvalidities }
   7527 						] );
   7528 						api.state( 'saving' ).set( false );
   7529 						return deferred.promise();
   7530 					}
   7531 
   7532 					/*
   7533 					 * Note that excludeCustomizedSaved is intentionally false so that the entire
   7534 					 * set of customized data will be included if bypassed changeset update.
   7535 					 */
   7536 					query = $.extend( previewer.query( { excludeCustomizedSaved: false } ), {
   7537 						nonce: previewer.nonce.save,
   7538 						customize_changeset_status: changesetStatus
   7539 					} );
   7540 
   7541 					if ( args && args.date ) {
   7542 						query.customize_changeset_date = args.date;
   7543 					} else if ( 'future' === changesetStatus && selectedChangesetDate ) {
   7544 						query.customize_changeset_date = selectedChangesetDate;
   7545 					}
   7546 
   7547 					if ( args && args.title ) {
   7548 						query.customize_changeset_title = args.title;
   7549 					}
   7550 
   7551 					// Allow plugins to modify the params included with the save request.
   7552 					api.trigger( 'save-request-params', query );
   7553 
   7554 					/*
   7555 					 * Note that the dirty customized values will have already been set in the
   7556 					 * changeset and so technically query.customized could be deleted. However,
   7557 					 * it is remaining here to make sure that any settings that got updated
   7558 					 * quietly which may have not triggered an update request will also get
   7559 					 * included in the values that get saved to the changeset. This will ensure
   7560 					 * that values that get injected via the saved event will be included in
   7561 					 * the changeset. This also ensures that setting values that were invalid
   7562 					 * will get re-validated, perhaps in the case of settings that are invalid
   7563 					 * due to dependencies on other settings.
   7564 					 */
   7565 					request = wp.ajax.post( 'customize_save', query );
   7566 					api.state( 'processing' ).set( api.state( 'processing' ).get() + 1 );
   7567 
   7568 					api.trigger( 'save', request );
   7569 
   7570 					request.always( function () {
   7571 						api.state( 'processing' ).set( api.state( 'processing' ).get() - 1 );
   7572 						api.state( 'saving' ).set( false );
   7573 						api.unbind( 'change', captureSettingModifiedDuringSave );
   7574 					} );
   7575 
   7576 					// Remove notifications that were added due to save failures.
   7577 					api.notifications.each( function( notification ) {
   7578 						if ( notification.saveFailure ) {
   7579 							api.notifications.remove( notification.code );
   7580 						}
   7581 					});
   7582 
   7583 					request.fail( function ( response ) {
   7584 						var notification, notificationArgs;
   7585 						notificationArgs = {
   7586 							type: 'error',
   7587 							dismissible: true,
   7588 							fromServer: true,
   7589 							saveFailure: true
   7590 						};
   7591 
   7592 						if ( '0' === response ) {
   7593 							response = 'not_logged_in';
   7594 						} else if ( '-1' === response ) {
   7595 							// Back-compat in case any other check_ajax_referer() call is dying.
   7596 							response = 'invalid_nonce';
   7597 						}
   7598 
   7599 						if ( 'invalid_nonce' === response ) {
   7600 							previewer.cheatin();
   7601 						} else if ( 'not_logged_in' === response ) {
   7602 							previewer.preview.iframe.hide();
   7603 							previewer.login().done( function() {
   7604 								previewer.save();
   7605 								previewer.preview.iframe.show();
   7606 							} );
   7607 						} else if ( response.code ) {
   7608 							if ( 'not_future_date' === response.code && api.section.has( 'publish_settings' ) && api.section( 'publish_settings' ).active.get() && api.control.has( 'changeset_scheduled_date' ) ) {
   7609 								api.control( 'changeset_scheduled_date' ).toggleFutureDateNotification( true ).focus();
   7610 							} else if ( 'changeset_locked' !== response.code ) {
   7611 								notification = new api.Notification( response.code, _.extend( notificationArgs, {
   7612 									message: response.message
   7613 								} ) );
   7614 							}
   7615 						} else {
   7616 							notification = new api.Notification( 'unknown_error', _.extend( notificationArgs, {
   7617 								message: api.l10n.unknownRequestFail
   7618 							} ) );
   7619 						}
   7620 
   7621 						if ( notification ) {
   7622 							api.notifications.add( notification );
   7623 						}
   7624 
   7625 						if ( response.setting_validities ) {
   7626 							api._handleSettingValidities( {
   7627 								settingValidities: response.setting_validities,
   7628 								focusInvalidControl: true
   7629 							} );
   7630 						}
   7631 
   7632 						deferred.rejectWith( previewer, [ response ] );
   7633 						api.trigger( 'error', response );
   7634 
   7635 						// Start a new changeset if the underlying changeset was published.
   7636 						if ( 'changeset_already_published' === response.code && response.next_changeset_uuid ) {
   7637 							api.settings.changeset.uuid = response.next_changeset_uuid;
   7638 							api.state( 'changesetStatus' ).set( '' );
   7639 							if ( api.settings.changeset.branching ) {
   7640 								parent.send( 'changeset-uuid', api.settings.changeset.uuid );
   7641 							}
   7642 							api.previewer.send( 'changeset-uuid', api.settings.changeset.uuid );
   7643 						}
   7644 					} );
   7645 
   7646 					request.done( function( response ) {
   7647 
   7648 						previewer.send( 'saved', response );
   7649 
   7650 						api.state( 'changesetStatus' ).set( response.changeset_status );
   7651 						if ( response.changeset_date ) {
   7652 							api.state( 'changesetDate' ).set( response.changeset_date );
   7653 						}
   7654 
   7655 						if ( 'publish' === response.changeset_status ) {
   7656 
   7657 							// Mark all published as clean if they haven't been modified during the request.
   7658 							api.each( function( setting ) {
   7659 								/*
   7660 								 * Note that the setting revision will be undefined in the case of setting
   7661 								 * values that are marked as dirty when the customizer is loaded, such as
   7662 								 * when applying starter content. All other dirty settings will have an
   7663 								 * associated revision due to their modification triggering a change event.
   7664 								 */
   7665 								if ( setting._dirty && ( _.isUndefined( api._latestSettingRevisions[ setting.id ] ) || api._latestSettingRevisions[ setting.id ] <= latestRevision ) ) {
   7666 									setting._dirty = false;
   7667 								}
   7668 							} );
   7669 
   7670 							api.state( 'changesetStatus' ).set( '' );
   7671 							api.settings.changeset.uuid = response.next_changeset_uuid;
   7672 							if ( api.settings.changeset.branching ) {
   7673 								parent.send( 'changeset-uuid', api.settings.changeset.uuid );
   7674 							}
   7675 						}
   7676 
   7677 						// Prevent subsequent requestChangesetUpdate() calls from including the settings that have been saved.
   7678 						api._lastSavedRevision = Math.max( latestRevision, api._lastSavedRevision );
   7679 
   7680 						if ( response.setting_validities ) {
   7681 							api._handleSettingValidities( {
   7682 								settingValidities: response.setting_validities,
   7683 								focusInvalidControl: true
   7684 							} );
   7685 						}
   7686 
   7687 						deferred.resolveWith( previewer, [ response ] );
   7688 						api.trigger( 'saved', response );
   7689 
   7690 						// Restore the global dirty state if any settings were modified during save.
   7691 						if ( ! _.isEmpty( modifiedWhileSaving ) ) {
   7692 							api.state( 'saved' ).set( false );
   7693 						}
   7694 					} );
   7695 				};
   7696 
   7697 				if ( 0 === processing() ) {
   7698 					submit();
   7699 				} else {
   7700 					submitWhenDoneProcessing = function () {
   7701 						if ( 0 === processing() ) {
   7702 							api.state.unbind( 'change', submitWhenDoneProcessing );
   7703 							submit();
   7704 						}
   7705 					};
   7706 					api.state.bind( 'change', submitWhenDoneProcessing );
   7707 				}
   7708 
   7709 				return deferred.promise();
   7710 			},
   7711 
   7712 			/**
   7713 			 * Trash the current changes.
   7714 			 *
   7715 			 * Revert the Customizer to its previously-published state.
   7716 			 *
   7717 			 * @since 4.9.0
   7718 			 *
   7719 			 * @return {jQuery.promise} Promise.
   7720 			 */
   7721 			trash: function trash() {
   7722 				var request, success, fail;
   7723 
   7724 				api.state( 'trashing' ).set( true );
   7725 				api.state( 'processing' ).set( api.state( 'processing' ).get() + 1 );
   7726 
   7727 				request = wp.ajax.post( 'customize_trash', {
   7728 					customize_changeset_uuid: api.settings.changeset.uuid,
   7729 					nonce: api.settings.nonce.trash
   7730 				} );
   7731 				api.notifications.add( new api.OverlayNotification( 'changeset_trashing', {
   7732 					type: 'info',
   7733 					message: api.l10n.revertingChanges,
   7734 					loading: true
   7735 				} ) );
   7736 
   7737 				success = function() {
   7738 					var urlParser = document.createElement( 'a' ), queryParams;
   7739 
   7740 					api.state( 'changesetStatus' ).set( 'trash' );
   7741 					api.each( function( setting ) {
   7742 						setting._dirty = false;
   7743 					} );
   7744 					api.state( 'saved' ).set( true );
   7745 
   7746 					// Go back to Customizer without changeset.
   7747 					urlParser.href = location.href;
   7748 					queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
   7749 					delete queryParams.changeset_uuid;
   7750 					queryParams['return'] = api.settings.url['return'];
   7751 					urlParser.search = $.param( queryParams );
   7752 					location.replace( urlParser.href );
   7753 				};
   7754 
   7755 				fail = function( code, message ) {
   7756 					var notificationCode = code || 'unknown_error';
   7757 					api.state( 'processing' ).set( api.state( 'processing' ).get() - 1 );
   7758 					api.state( 'trashing' ).set( false );
   7759 					api.notifications.remove( 'changeset_trashing' );
   7760 					api.notifications.add( new api.Notification( notificationCode, {
   7761 						message: message || api.l10n.unknownError,
   7762 						dismissible: true,
   7763 						type: 'error'
   7764 					} ) );
   7765 				};
   7766 
   7767 				request.done( function( response ) {
   7768 					success( response.message );
   7769 				} );
   7770 
   7771 				request.fail( function( response ) {
   7772 					var code = response.code || 'trashing_failed';
   7773 					if ( response.success || 'non_existent_changeset' === code || 'changeset_already_trashed' === code ) {
   7774 						success( response.message );
   7775 					} else {
   7776 						fail( code, response.message );
   7777 					}
   7778 				} );
   7779 			},
   7780 
   7781 			/**
   7782 			 * Builds the front preview url with the current state of customizer.
   7783 			 *
   7784 			 * @since 4.9
   7785 			 *
   7786 			 * @return {string} Preview url.
   7787 			 */
   7788 			getFrontendPreviewUrl: function() {
   7789 				var previewer = this, params, urlParser;
   7790 				urlParser = document.createElement( 'a' );
   7791 				urlParser.href = previewer.previewUrl.get();
   7792 				params = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
   7793 
   7794 				if ( api.state( 'changesetStatus' ).get() && 'publish' !== api.state( 'changesetStatus' ).get() ) {
   7795 					params.customize_changeset_uuid = api.settings.changeset.uuid;
   7796 				}
   7797 				if ( ! api.state( 'activated' ).get() ) {
   7798 					params.customize_theme = api.settings.theme.stylesheet;
   7799 				}
   7800 
   7801 				urlParser.search = $.param( params );
   7802 				return urlParser.href;
   7803 			}
   7804 		});
   7805 
   7806 		// Ensure preview nonce is included with every customized request, to allow post data to be read.
   7807 		$.ajaxPrefilter( function injectPreviewNonce( options ) {
   7808 			if ( ! /wp_customize=on/.test( options.data ) ) {
   7809 				return;
   7810 			}
   7811 			options.data += '&' + $.param({
   7812 				customize_preview_nonce: api.settings.nonce.preview
   7813 			});
   7814 		});
   7815 
   7816 		// Refresh the nonces if the preview sends updated nonces over.
   7817 		api.previewer.bind( 'nonce', function( nonce ) {
   7818 			$.extend( this.nonce, nonce );
   7819 		});
   7820 
   7821 		// Refresh the nonces if login sends updated nonces over.
   7822 		api.bind( 'nonce-refresh', function( nonce ) {
   7823 			$.extend( api.settings.nonce, nonce );
   7824 			$.extend( api.previewer.nonce, nonce );
   7825 			api.previewer.send( 'nonce-refresh', nonce );
   7826 		});
   7827 
   7828 		// Create Settings.
   7829 		$.each( api.settings.settings, function( id, data ) {
   7830 			var Constructor = api.settingConstructor[ data.type ] || api.Setting;
   7831 			api.add( new Constructor( id, data.value, {
   7832 				transport: data.transport,
   7833 				previewer: api.previewer,
   7834 				dirty: !! data.dirty
   7835 			} ) );
   7836 		});
   7837 
   7838 		// Create Panels.
   7839 		$.each( api.settings.panels, function ( id, data ) {
   7840 			var Constructor = api.panelConstructor[ data.type ] || api.Panel, options;
   7841 			// Inclusion of params alias is for back-compat for custom panels that expect to augment this property.
   7842 			options = _.extend( { params: data }, data );
   7843 			api.panel.add( new Constructor( id, options ) );
   7844 		});
   7845 
   7846 		// Create Sections.
   7847 		$.each( api.settings.sections, function ( id, data ) {
   7848 			var Constructor = api.sectionConstructor[ data.type ] || api.Section, options;
   7849 			// Inclusion of params alias is for back-compat for custom sections that expect to augment this property.
   7850 			options = _.extend( { params: data }, data );
   7851 			api.section.add( new Constructor( id, options ) );
   7852 		});
   7853 
   7854 		// Create Controls.
   7855 		$.each( api.settings.controls, function( id, data ) {
   7856 			var Constructor = api.controlConstructor[ data.type ] || api.Control, options;
   7857 			// Inclusion of params alias is for back-compat for custom controls that expect to augment this property.
   7858 			options = _.extend( { params: data }, data );
   7859 			api.control.add( new Constructor( id, options ) );
   7860 		});
   7861 
   7862 		// Focus the autofocused element.
   7863 		_.each( [ 'panel', 'section', 'control' ], function( type ) {
   7864 			var id = api.settings.autofocus[ type ];
   7865 			if ( ! id ) {
   7866 				return;
   7867 			}
   7868 
   7869 			/*
   7870 			 * Defer focus until:
   7871 			 * 1. The panel, section, or control exists (especially for dynamically-created ones).
   7872 			 * 2. The instance is embedded in the document (and so is focusable).
   7873 			 * 3. The preview has finished loading so that the active states have been set.
   7874 			 */
   7875 			api[ type ]( id, function( instance ) {
   7876 				instance.deferred.embedded.done( function() {
   7877 					api.previewer.deferred.active.done( function() {
   7878 						instance.focus();
   7879 					});
   7880 				});
   7881 			});
   7882 		});
   7883 
   7884 		api.bind( 'ready', api.reflowPaneContents );
   7885 		$( [ api.panel, api.section, api.control ] ).each( function ( i, values ) {
   7886 			var debouncedReflowPaneContents = _.debounce( api.reflowPaneContents, api.settings.timeouts.reflowPaneContents );
   7887 			values.bind( 'add', debouncedReflowPaneContents );
   7888 			values.bind( 'change', debouncedReflowPaneContents );
   7889 			values.bind( 'remove', debouncedReflowPaneContents );
   7890 		} );
   7891 
   7892 		// Set up global notifications area.
   7893 		api.bind( 'ready', function setUpGlobalNotificationsArea() {
   7894 			var sidebar, containerHeight, containerInitialTop;
   7895 			api.notifications.container = $( '#customize-notifications-area' );
   7896 
   7897 			api.notifications.bind( 'change', _.debounce( function() {
   7898 				api.notifications.render();
   7899 			} ) );
   7900 
   7901 			sidebar = $( '.wp-full-overlay-sidebar-content' );
   7902 			api.notifications.bind( 'rendered', function updateSidebarTop() {
   7903 				sidebar.css( 'top', '' );
   7904 				if ( 0 !== api.notifications.count() ) {
   7905 					containerHeight = api.notifications.container.outerHeight() + 1;
   7906 					containerInitialTop = parseInt( sidebar.css( 'top' ), 10 );
   7907 					sidebar.css( 'top', containerInitialTop + containerHeight + 'px' );
   7908 				}
   7909 				api.notifications.trigger( 'sidebarTopUpdated' );
   7910 			});
   7911 
   7912 			api.notifications.render();
   7913 		});
   7914 
   7915 		// Save and activated states.
   7916 		(function( state ) {
   7917 			var saved = state.instance( 'saved' ),
   7918 				saving = state.instance( 'saving' ),
   7919 				trashing = state.instance( 'trashing' ),
   7920 				activated = state.instance( 'activated' ),
   7921 				processing = state.instance( 'processing' ),
   7922 				paneVisible = state.instance( 'paneVisible' ),
   7923 				expandedPanel = state.instance( 'expandedPanel' ),
   7924 				expandedSection = state.instance( 'expandedSection' ),
   7925 				changesetStatus = state.instance( 'changesetStatus' ),
   7926 				selectedChangesetStatus = state.instance( 'selectedChangesetStatus' ),
   7927 				changesetDate = state.instance( 'changesetDate' ),
   7928 				selectedChangesetDate = state.instance( 'selectedChangesetDate' ),
   7929 				previewerAlive = state.instance( 'previewerAlive' ),
   7930 				editShortcutVisibility  = state.instance( 'editShortcutVisibility' ),
   7931 				changesetLocked = state.instance( 'changesetLocked' ),
   7932 				populateChangesetUuidParam, defaultSelectedChangesetStatus;
   7933 
   7934 			state.bind( 'change', function() {
   7935 				var canSave;
   7936 
   7937 				if ( ! activated() ) {
   7938 					saveBtn.val( api.l10n.activate );
   7939 					closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
   7940 
   7941 				} else if ( '' === changesetStatus.get() && saved() ) {
   7942 					if ( api.settings.changeset.currentUserCanPublish ) {
   7943 						saveBtn.val( api.l10n.published );
   7944 					} else {
   7945 						saveBtn.val( api.l10n.saved );
   7946 					}
   7947 					closeBtn.find( '.screen-reader-text' ).text( api.l10n.close );
   7948 
   7949 				} else {
   7950 					if ( 'draft' === selectedChangesetStatus() ) {
   7951 						if ( saved() && selectedChangesetStatus() === changesetStatus() ) {
   7952 							saveBtn.val( api.l10n.draftSaved );
   7953 						} else {
   7954 							saveBtn.val( api.l10n.saveDraft );
   7955 						}
   7956 					} else if ( 'future' === selectedChangesetStatus() ) {
   7957 						if ( saved() && selectedChangesetStatus() === changesetStatus() ) {
   7958 							if ( changesetDate.get() !== selectedChangesetDate.get() ) {
   7959 								saveBtn.val( api.l10n.schedule );
   7960 							} else {
   7961 								saveBtn.val( api.l10n.scheduled );
   7962 							}
   7963 						} else {
   7964 							saveBtn.val( api.l10n.schedule );
   7965 						}
   7966 					} else if ( api.settings.changeset.currentUserCanPublish ) {
   7967 						saveBtn.val( api.l10n.publish );
   7968 					}
   7969 					closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
   7970 				}
   7971 
   7972 				/*
   7973 				 * Save (publish) button should be enabled if saving is not currently happening,
   7974 				 * and if the theme is not active or the changeset exists but is not published.
   7975 				 */
   7976 				canSave = ! saving() && ! trashing() && ! changesetLocked() && ( ! activated() || ! saved() || ( changesetStatus() !== selectedChangesetStatus() && '' !== changesetStatus() ) || ( 'future' === selectedChangesetStatus() && changesetDate.get() !== selectedChangesetDate.get() ) );
   7977 
   7978 				saveBtn.prop( 'disabled', ! canSave );
   7979 			});
   7980 
   7981 			selectedChangesetStatus.validate = function( status ) {
   7982 				if ( '' === status || 'auto-draft' === status ) {
   7983 					return null;
   7984 				}
   7985 				return status;
   7986 			};
   7987 
   7988 			defaultSelectedChangesetStatus = api.settings.changeset.currentUserCanPublish ? 'publish' : 'draft';
   7989 
   7990 			// Set default states.
   7991 			changesetStatus( api.settings.changeset.status );
   7992 			changesetLocked( Boolean( api.settings.changeset.lockUser ) );
   7993 			changesetDate( api.settings.changeset.publishDate );
   7994 			selectedChangesetDate( api.settings.changeset.publishDate );
   7995 			selectedChangesetStatus( '' === api.settings.changeset.status || 'auto-draft' === api.settings.changeset.status ? defaultSelectedChangesetStatus : api.settings.changeset.status );
   7996 			selectedChangesetStatus.link( changesetStatus ); // Ensure that direct updates to status on server via wp.customizer.previewer.save() will update selection.
   7997 			saved( true );
   7998 			if ( '' === changesetStatus() ) { // Handle case for loading starter content.
   7999 				api.each( function( setting ) {
   8000 					if ( setting._dirty ) {
   8001 						saved( false );
   8002 					}
   8003 				} );
   8004 			}
   8005 			saving( false );
   8006 			activated( api.settings.theme.active );
   8007 			processing( 0 );
   8008 			paneVisible( true );
   8009 			expandedPanel( false );
   8010 			expandedSection( false );
   8011 			previewerAlive( true );
   8012 			editShortcutVisibility( 'visible' );
   8013 
   8014 			api.bind( 'change', function() {
   8015 				if ( state( 'saved' ).get() ) {
   8016 					state( 'saved' ).set( false );
   8017 				}
   8018 			});
   8019 
   8020 			// Populate changeset UUID param when state becomes dirty.
   8021 			if ( api.settings.changeset.branching ) {
   8022 				saved.bind( function( isSaved ) {
   8023 					if ( ! isSaved ) {
   8024 						populateChangesetUuidParam( true );
   8025 					}
   8026 				});
   8027 			}
   8028 
   8029 			saving.bind( function( isSaving ) {
   8030 				body.toggleClass( 'saving', isSaving );
   8031 			} );
   8032 			trashing.bind( function( isTrashing ) {
   8033 				body.toggleClass( 'trashing', isTrashing );
   8034 			} );
   8035 
   8036 			api.bind( 'saved', function( response ) {
   8037 				state('saved').set( true );
   8038 				if ( 'publish' === response.changeset_status ) {
   8039 					state( 'activated' ).set( true );
   8040 				}
   8041 			});
   8042 
   8043 			activated.bind( function( to ) {
   8044 				if ( to ) {
   8045 					api.trigger( 'activated' );
   8046 				}
   8047 			});
   8048 
   8049 			/**
   8050 			 * Populate URL with UUID via `history.replaceState()`.
   8051 			 *
   8052 			 * @since 4.7.0
   8053 			 * @access private
   8054 			 *
   8055 			 * @param {boolean} isIncluded Is UUID included.
   8056 			 * @return {void}
   8057 			 */
   8058 			populateChangesetUuidParam = function( isIncluded ) {
   8059 				var urlParser, queryParams;
   8060 
   8061 				// Abort on IE9 which doesn't support history management.
   8062 				if ( ! history.replaceState ) {
   8063 					return;
   8064 				}
   8065 
   8066 				urlParser = document.createElement( 'a' );
   8067 				urlParser.href = location.href;
   8068 				queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
   8069 				if ( isIncluded ) {
   8070 					if ( queryParams.changeset_uuid === api.settings.changeset.uuid ) {
   8071 						return;
   8072 					}
   8073 					queryParams.changeset_uuid = api.settings.changeset.uuid;
   8074 				} else {
   8075 					if ( ! queryParams.changeset_uuid ) {
   8076 						return;
   8077 					}
   8078 					delete queryParams.changeset_uuid;
   8079 				}
   8080 				urlParser.search = $.param( queryParams );
   8081 				history.replaceState( {}, document.title, urlParser.href );
   8082 			};
   8083 
   8084 			// Show changeset UUID in URL when in branching mode and there is a saved changeset.
   8085 			if ( api.settings.changeset.branching ) {
   8086 				changesetStatus.bind( function( newStatus ) {
   8087 					populateChangesetUuidParam( '' !== newStatus && 'publish' !== newStatus && 'trash' !== newStatus );
   8088 				} );
   8089 			}
   8090 		}( api.state ) );
   8091 
   8092 		/**
   8093 		 * Handles lock notice and take over request.
   8094 		 *
   8095 		 * @since 4.9.0
   8096 		 */
   8097 		( function checkAndDisplayLockNotice() {
   8098 
   8099 			var LockedNotification = api.OverlayNotification.extend(/** @lends wp.customize~LockedNotification.prototype */{
   8100 
   8101 				/**
   8102 				 * Template ID.
   8103 				 *
   8104 				 * @type {string}
   8105 				 */
   8106 				templateId: 'customize-changeset-locked-notification',
   8107 
   8108 				/**
   8109 				 * Lock user.
   8110 				 *
   8111 				 * @type {object}
   8112 				 */
   8113 				lockUser: null,
   8114 
   8115 				/**
   8116 				 * A notification that is displayed in a full-screen overlay with information about the locked changeset.
   8117 				 *
   8118 				 * @constructs wp.customize~LockedNotification
   8119 				 * @augments   wp.customize.OverlayNotification
   8120 				 *
   8121 				 * @since 4.9.0
   8122 				 *
   8123 				 * @param {string} [code] - Code.
   8124 				 * @param {Object} [params] - Params.
   8125 				 */
   8126 				initialize: function( code, params ) {
   8127 					var notification = this, _code, _params;
   8128 					_code = code || 'changeset_locked';
   8129 					_params = _.extend(
   8130 						{
   8131 							message: '',
   8132 							type: 'warning',
   8133 							containerClasses: '',
   8134 							lockUser: {}
   8135 						},
   8136 						params
   8137 					);
   8138 					_params.containerClasses += ' notification-changeset-locked';
   8139 					api.OverlayNotification.prototype.initialize.call( notification, _code, _params );
   8140 				},
   8141 
   8142 				/**
   8143 				 * Render notification.
   8144 				 *
   8145 				 * @since 4.9.0
   8146 				 *
   8147 				 * @return {jQuery} Notification container.
   8148 				 */
   8149 				render: function() {
   8150 					var notification = this, li, data, takeOverButton, request;
   8151 					data = _.extend(
   8152 						{
   8153 							allowOverride: false,
   8154 							returnUrl: api.settings.url['return'],
   8155 							previewUrl: api.previewer.previewUrl.get(),
   8156 							frontendPreviewUrl: api.previewer.getFrontendPreviewUrl()
   8157 						},
   8158 						this
   8159 					);
   8160 
   8161 					li = api.OverlayNotification.prototype.render.call( data );
   8162 
   8163 					// Try to autosave the changeset now.
   8164 					api.requestChangesetUpdate( {}, { autosave: true } ).fail( function( response ) {
   8165 						if ( ! response.autosaved ) {
   8166 							li.find( '.notice-error' ).prop( 'hidden', false ).text( response.message || api.l10n.unknownRequestFail );
   8167 						}
   8168 					} );
   8169 
   8170 					takeOverButton = li.find( '.customize-notice-take-over-button' );
   8171 					takeOverButton.on( 'click', function( event ) {
   8172 						event.preventDefault();
   8173 						if ( request ) {
   8174 							return;
   8175 						}
   8176 
   8177 						takeOverButton.addClass( 'disabled' );
   8178 						request = wp.ajax.post( 'customize_override_changeset_lock', {
   8179 							wp_customize: 'on',
   8180 							customize_theme: api.settings.theme.stylesheet,
   8181 							customize_changeset_uuid: api.settings.changeset.uuid,
   8182 							nonce: api.settings.nonce.override_lock
   8183 						} );
   8184 
   8185 						request.done( function() {
   8186 							api.notifications.remove( notification.code ); // Remove self.
   8187 							api.state( 'changesetLocked' ).set( false );
   8188 						} );
   8189 
   8190 						request.fail( function( response ) {
   8191 							var message = response.message || api.l10n.unknownRequestFail;
   8192 							li.find( '.notice-error' ).prop( 'hidden', false ).text( message );
   8193 
   8194 							request.always( function() {
   8195 								takeOverButton.removeClass( 'disabled' );
   8196 							} );
   8197 						} );
   8198 
   8199 						request.always( function() {
   8200 							request = null;
   8201 						} );
   8202 					} );
   8203 
   8204 					return li;
   8205 				}
   8206 			});
   8207 
   8208 			/**
   8209 			 * Start lock.
   8210 			 *
   8211 			 * @since 4.9.0
   8212 			 *
   8213 			 * @param {Object} [args] - Args.
   8214 			 * @param {Object} [args.lockUser] - Lock user data.
   8215 			 * @param {boolean} [args.allowOverride=false] - Whether override is allowed.
   8216 			 * @return {void}
   8217 			 */
   8218 			function startLock( args ) {
   8219 				if ( args && args.lockUser ) {
   8220 					api.settings.changeset.lockUser = args.lockUser;
   8221 				}
   8222 				api.state( 'changesetLocked' ).set( true );
   8223 				api.notifications.add( new LockedNotification( 'changeset_locked', {
   8224 					lockUser: api.settings.changeset.lockUser,
   8225 					allowOverride: Boolean( args && args.allowOverride )
   8226 				} ) );
   8227 			}
   8228 
   8229 			// Show initial notification.
   8230 			if ( api.settings.changeset.lockUser ) {
   8231 				startLock( { allowOverride: true } );
   8232 			}
   8233 
   8234 			// Check for lock when sending heartbeat requests.
   8235 			$( document ).on( 'heartbeat-send.update_lock_notice', function( event, data ) {
   8236 				data.check_changeset_lock = true;
   8237 				data.changeset_uuid = api.settings.changeset.uuid;
   8238 			} );
   8239 
   8240 			// Handle heartbeat ticks.
   8241 			$( document ).on( 'heartbeat-tick.update_lock_notice', function( event, data ) {
   8242 				var notification, code = 'changeset_locked';
   8243 				if ( ! data.customize_changeset_lock_user ) {
   8244 					return;
   8245 				}
   8246 
   8247 				// Update notification when a different user takes over.
   8248 				notification = api.notifications( code );
   8249 				if ( notification && notification.lockUser.id !== api.settings.changeset.lockUser.id ) {
   8250 					api.notifications.remove( code );
   8251 				}
   8252 
   8253 				startLock( {
   8254 					lockUser: data.customize_changeset_lock_user
   8255 				} );
   8256 			} );
   8257 
   8258 			// Handle locking in response to changeset save errors.
   8259 			api.bind( 'error', function( response ) {
   8260 				if ( 'changeset_locked' === response.code && response.lock_user ) {
   8261 					startLock( {
   8262 						lockUser: response.lock_user
   8263 					} );
   8264 				}
   8265 			} );
   8266 		} )();
   8267 
   8268 		// Set up initial notifications.
   8269 		(function() {
   8270 			var removedQueryParams = [], autosaveDismissed = false;
   8271 
   8272 			/**
   8273 			 * Obtain the URL to restore the autosave.
   8274 			 *
   8275 			 * @return {string} Customizer URL.
   8276 			 */
   8277 			function getAutosaveRestorationUrl() {
   8278 				var urlParser, queryParams;
   8279 				urlParser = document.createElement( 'a' );
   8280 				urlParser.href = location.href;
   8281 				queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
   8282 				if ( api.settings.changeset.latestAutoDraftUuid ) {
   8283 					queryParams.changeset_uuid = api.settings.changeset.latestAutoDraftUuid;
   8284 				} else {
   8285 					queryParams.customize_autosaved = 'on';
   8286 				}
   8287 				queryParams['return'] = api.settings.url['return'];
   8288 				urlParser.search = $.param( queryParams );
   8289 				return urlParser.href;
   8290 			}
   8291 
   8292 			/**
   8293 			 * Remove parameter from the URL.
   8294 			 *
   8295 			 * @param {Array} params - Parameter names to remove.
   8296 			 * @return {void}
   8297 			 */
   8298 			function stripParamsFromLocation( params ) {
   8299 				var urlParser = document.createElement( 'a' ), queryParams, strippedParams = 0;
   8300 				urlParser.href = location.href;
   8301 				queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
   8302 				_.each( params, function( param ) {
   8303 					if ( 'undefined' !== typeof queryParams[ param ] ) {
   8304 						strippedParams += 1;
   8305 						delete queryParams[ param ];
   8306 					}
   8307 				} );
   8308 				if ( 0 === strippedParams ) {
   8309 					return;
   8310 				}
   8311 
   8312 				urlParser.search = $.param( queryParams );
   8313 				history.replaceState( {}, document.title, urlParser.href );
   8314 			}
   8315 
   8316 			/**
   8317 			 * Dismiss autosave.
   8318 			 *
   8319 			 * @return {void}
   8320 			 */
   8321 			function dismissAutosave() {
   8322 				if ( autosaveDismissed ) {
   8323 					return;
   8324 				}
   8325 				wp.ajax.post( 'customize_dismiss_autosave_or_lock', {
   8326 					wp_customize: 'on',
   8327 					customize_theme: api.settings.theme.stylesheet,
   8328 					customize_changeset_uuid: api.settings.changeset.uuid,
   8329 					nonce: api.settings.nonce.dismiss_autosave_or_lock,
   8330 					dismiss_autosave: true
   8331 				} );
   8332 				autosaveDismissed = true;
   8333 			}
   8334 
   8335 			/**
   8336 			 * Add notification regarding the availability of an autosave to restore.
   8337 			 *
   8338 			 * @return {void}
   8339 			 */
   8340 			function addAutosaveRestoreNotification() {
   8341 				var code = 'autosave_available', onStateChange;
   8342 
   8343 				// Since there is an autosave revision and the user hasn't loaded with autosaved, add notification to prompt to load autosaved version.
   8344 				api.notifications.add( new api.Notification( code, {
   8345 					message: api.l10n.autosaveNotice,
   8346 					type: 'warning',
   8347 					dismissible: true,
   8348 					render: function() {
   8349 						var li = api.Notification.prototype.render.call( this ), link;
   8350 
   8351 						// Handle clicking on restoration link.
   8352 						link = li.find( 'a' );
   8353 						link.prop( 'href', getAutosaveRestorationUrl() );
   8354 						link.on( 'click', function( event ) {
   8355 							event.preventDefault();
   8356 							location.replace( getAutosaveRestorationUrl() );
   8357 						} );
   8358 
   8359 						// Handle dismissal of notice.
   8360 						li.find( '.notice-dismiss' ).on( 'click', dismissAutosave );
   8361 
   8362 						return li;
   8363 					}
   8364 				} ) );
   8365 
   8366 				// Remove the notification once the user starts making changes.
   8367 				onStateChange = function() {
   8368 					dismissAutosave();
   8369 					api.notifications.remove( code );
   8370 					api.unbind( 'change', onStateChange );
   8371 					api.state( 'changesetStatus' ).unbind( onStateChange );
   8372 				};
   8373 				api.bind( 'change', onStateChange );
   8374 				api.state( 'changesetStatus' ).bind( onStateChange );
   8375 			}
   8376 
   8377 			if ( api.settings.changeset.autosaved ) {
   8378 				api.state( 'saved' ).set( false );
   8379 				removedQueryParams.push( 'customize_autosaved' );
   8380 			}
   8381 			if ( ! api.settings.changeset.branching && ( ! api.settings.changeset.status || 'auto-draft' === api.settings.changeset.status ) ) {
   8382 				removedQueryParams.push( 'changeset_uuid' ); // Remove UUID when restoring autosave auto-draft.
   8383 			}
   8384 			if ( removedQueryParams.length > 0 ) {
   8385 				stripParamsFromLocation( removedQueryParams );
   8386 			}
   8387 			if ( api.settings.changeset.latestAutoDraftUuid || api.settings.changeset.hasAutosaveRevision ) {
   8388 				addAutosaveRestoreNotification();
   8389 			}
   8390 		})();
   8391 
   8392 		// Check if preview url is valid and load the preview frame.
   8393 		if ( api.previewer.previewUrl() ) {
   8394 			api.previewer.refresh();
   8395 		} else {
   8396 			api.previewer.previewUrl( api.settings.url.home );
   8397 		}
   8398 
   8399 		// Button bindings.
   8400 		saveBtn.on( 'click', function( event ) {
   8401 			api.previewer.save();
   8402 			event.preventDefault();
   8403 		}).on( 'keydown', function( event ) {
   8404 			if ( 9 === event.which ) { // Tab.
   8405 				return;
   8406 			}
   8407 			if ( 13 === event.which ) { // Enter.
   8408 				api.previewer.save();
   8409 			}
   8410 			event.preventDefault();
   8411 		});
   8412 
   8413 		closeBtn.on( 'keydown', function( event ) {
   8414 			if ( 9 === event.which ) { // Tab.
   8415 				return;
   8416 			}
   8417 			if ( 13 === event.which ) { // Enter.
   8418 				this.click();
   8419 			}
   8420 			event.preventDefault();
   8421 		});
   8422 
   8423 		$( '.collapse-sidebar' ).on( 'click', function() {
   8424 			api.state( 'paneVisible' ).set( ! api.state( 'paneVisible' ).get() );
   8425 		});
   8426 
   8427 		api.state( 'paneVisible' ).bind( function( paneVisible ) {
   8428 			overlay.toggleClass( 'preview-only', ! paneVisible );
   8429 			overlay.toggleClass( 'expanded', paneVisible );
   8430 			overlay.toggleClass( 'collapsed', ! paneVisible );
   8431 
   8432 			if ( ! paneVisible ) {
   8433 				$( '.collapse-sidebar' ).attr({ 'aria-expanded': 'false', 'aria-label': api.l10n.expandSidebar });
   8434 			} else {
   8435 				$( '.collapse-sidebar' ).attr({ 'aria-expanded': 'true', 'aria-label': api.l10n.collapseSidebar });
   8436 			}
   8437 		});
   8438 
   8439 		// Keyboard shortcuts - esc to exit section/panel.
   8440 		body.on( 'keydown', function( event ) {
   8441 			var collapsedObject, expandedControls = [], expandedSections = [], expandedPanels = [];
   8442 
   8443 			if ( 27 !== event.which ) { // Esc.
   8444 				return;
   8445 			}
   8446 
   8447 			/*
   8448 			 * Abort if the event target is not the body (the default) and not inside of #customize-controls.
   8449 			 * This ensures that ESC meant to collapse a modal dialog or a TinyMCE toolbar won't collapse something else.
   8450 			 */
   8451 			if ( ! $( event.target ).is( 'body' ) && ! $.contains( $( '#customize-controls' )[0], event.target ) ) {
   8452 				return;
   8453 			}
   8454 
   8455 			// Abort if we're inside of a block editor instance.
   8456 			if ( event.target.closest( '.block-editor-writing-flow' ) !== null ||
   8457 				event.target.closest( '.block-editor-block-list__block-popover' ) !== null
   8458 			) {
   8459 				return;
   8460 			}
   8461 
   8462 			// Check for expanded expandable controls (e.g. widgets and nav menus items), sections, and panels.
   8463 			api.control.each( function( control ) {
   8464 				if ( control.expanded && control.expanded() && _.isFunction( control.collapse ) ) {
   8465 					expandedControls.push( control );
   8466 				}
   8467 			});
   8468 			api.section.each( function( section ) {
   8469 				if ( section.expanded() ) {
   8470 					expandedSections.push( section );
   8471 				}
   8472 			});
   8473 			api.panel.each( function( panel ) {
   8474 				if ( panel.expanded() ) {
   8475 					expandedPanels.push( panel );
   8476 				}
   8477 			});
   8478 
   8479 			// Skip collapsing expanded controls if there are no expanded sections.
   8480 			if ( expandedControls.length > 0 && 0 === expandedSections.length ) {
   8481 				expandedControls.length = 0;
   8482 			}
   8483 
   8484 			// Collapse the most granular expanded object.
   8485 			collapsedObject = expandedControls[0] || expandedSections[0] || expandedPanels[0];
   8486 			if ( collapsedObject ) {
   8487 				if ( 'themes' === collapsedObject.params.type ) {
   8488 
   8489 					// Themes panel or section.
   8490 					if ( body.hasClass( 'modal-open' ) ) {
   8491 						collapsedObject.closeDetails();
   8492 					} else if ( api.panel.has( 'themes' ) ) {
   8493 
   8494 						// If we're collapsing a section, collapse the panel also.
   8495 						api.panel( 'themes' ).collapse();
   8496 					}
   8497 					return;
   8498 				}
   8499 				collapsedObject.collapse();
   8500 				event.preventDefault();
   8501 			}
   8502 		});
   8503 
   8504 		$( '.customize-controls-preview-toggle' ).on( 'click', function() {
   8505 			api.state( 'paneVisible' ).set( ! api.state( 'paneVisible' ).get() );
   8506 		});
   8507 
   8508 		/*
   8509 		 * Sticky header feature.
   8510 		 */
   8511 		(function initStickyHeaders() {
   8512 			var parentContainer = $( '.wp-full-overlay-sidebar-content' ),
   8513 				changeContainer, updateHeaderHeight, releaseStickyHeader, resetStickyHeader, positionStickyHeader,
   8514 				activeHeader, lastScrollTop;
   8515 
   8516 			/**
   8517 			 * Determine which panel or section is currently expanded.
   8518 			 *
   8519 			 * @since 4.7.0
   8520 			 * @access private
   8521 			 *
   8522 			 * @param {wp.customize.Panel|wp.customize.Section} container Construct.
   8523 			 * @return {void}
   8524 			 */
   8525 			changeContainer = function( container ) {
   8526 				var newInstance = container,
   8527 					expandedSection = api.state( 'expandedSection' ).get(),
   8528 					expandedPanel = api.state( 'expandedPanel' ).get(),
   8529 					headerElement;
   8530 
   8531 				if ( activeHeader && activeHeader.element ) {
   8532 					// Release previously active header element.
   8533 					releaseStickyHeader( activeHeader.element );
   8534 
   8535 					// Remove event listener in the previous panel or section.
   8536 					activeHeader.element.find( '.description' ).off( 'toggled', updateHeaderHeight );
   8537 				}
   8538 
   8539 				if ( ! newInstance ) {
   8540 					if ( ! expandedSection && expandedPanel && expandedPanel.contentContainer ) {
   8541 						newInstance = expandedPanel;
   8542 					} else if ( ! expandedPanel && expandedSection && expandedSection.contentContainer ) {
   8543 						newInstance = expandedSection;
   8544 					} else {
   8545 						activeHeader = false;
   8546 						return;
   8547 					}
   8548 				}
   8549 
   8550 				headerElement = newInstance.contentContainer.find( '.customize-section-title, .panel-meta' ).first();
   8551 				if ( headerElement.length ) {
   8552 					activeHeader = {
   8553 						instance: newInstance,
   8554 						element:  headerElement,
   8555 						parent:   headerElement.closest( '.customize-pane-child' ),
   8556 						height:   headerElement.outerHeight()
   8557 					};
   8558 
   8559 					// Update header height whenever help text is expanded or collapsed.
   8560 					activeHeader.element.find( '.description' ).on( 'toggled', updateHeaderHeight );
   8561 
   8562 					if ( expandedSection ) {
   8563 						resetStickyHeader( activeHeader.element, activeHeader.parent );
   8564 					}
   8565 				} else {
   8566 					activeHeader = false;
   8567 				}
   8568 			};
   8569 			api.state( 'expandedSection' ).bind( changeContainer );
   8570 			api.state( 'expandedPanel' ).bind( changeContainer );
   8571 
   8572 			// Throttled scroll event handler.
   8573 			parentContainer.on( 'scroll', _.throttle( function() {
   8574 				if ( ! activeHeader ) {
   8575 					return;
   8576 				}
   8577 
   8578 				var scrollTop = parentContainer.scrollTop(),
   8579 					scrollDirection;
   8580 
   8581 				if ( ! lastScrollTop ) {
   8582 					scrollDirection = 1;
   8583 				} else {
   8584 					if ( scrollTop === lastScrollTop ) {
   8585 						scrollDirection = 0;
   8586 					} else if ( scrollTop > lastScrollTop ) {
   8587 						scrollDirection = 1;
   8588 					} else {
   8589 						scrollDirection = -1;
   8590 					}
   8591 				}
   8592 				lastScrollTop = scrollTop;
   8593 				if ( 0 !== scrollDirection ) {
   8594 					positionStickyHeader( activeHeader, scrollTop, scrollDirection );
   8595 				}
   8596 			}, 8 ) );
   8597 
   8598 			// Update header position on sidebar layout change.
   8599 			api.notifications.bind( 'sidebarTopUpdated', function() {
   8600 				if ( activeHeader && activeHeader.element.hasClass( 'is-sticky' ) ) {
   8601 					activeHeader.element.css( 'top', parentContainer.css( 'top' ) );
   8602 				}
   8603 			});
   8604 
   8605 			// Release header element if it is sticky.
   8606 			releaseStickyHeader = function( headerElement ) {
   8607 				if ( ! headerElement.hasClass( 'is-sticky' ) ) {
   8608 					return;
   8609 				}
   8610 				headerElement
   8611 					.removeClass( 'is-sticky' )
   8612 					.addClass( 'maybe-sticky is-in-view' )
   8613 					.css( 'top', parentContainer.scrollTop() + 'px' );
   8614 			};
   8615 
   8616 			// Reset position of the sticky header.
   8617 			resetStickyHeader = function( headerElement, headerParent ) {
   8618 				if ( headerElement.hasClass( 'is-in-view' ) ) {
   8619 					headerElement
   8620 						.removeClass( 'maybe-sticky is-in-view' )
   8621 						.css( {
   8622 							width: '',
   8623 							top:   ''
   8624 						} );
   8625 					headerParent.css( 'padding-top', '' );
   8626 				}
   8627 			};
   8628 
   8629 			/**
   8630 			 * Update active header height.
   8631 			 *
   8632 			 * @since 4.7.0
   8633 			 * @access private
   8634 			 *
   8635 			 * @return {void}
   8636 			 */
   8637 			updateHeaderHeight = function() {
   8638 				activeHeader.height = activeHeader.element.outerHeight();
   8639 			};
   8640 
   8641 			/**
   8642 			 * Reposition header on throttled `scroll` event.
   8643 			 *
   8644 			 * @since 4.7.0
   8645 			 * @access private
   8646 			 *
   8647 			 * @param {Object} header - Header.
   8648 			 * @param {number} scrollTop - Scroll top.
   8649 			 * @param {number} scrollDirection - Scroll direction, negative number being up and positive being down.
   8650 			 * @return {void}
   8651 			 */
   8652 			positionStickyHeader = function( header, scrollTop, scrollDirection ) {
   8653 				var headerElement = header.element,
   8654 					headerParent = header.parent,
   8655 					headerHeight = header.height,
   8656 					headerTop = parseInt( headerElement.css( 'top' ), 10 ),
   8657 					maybeSticky = headerElement.hasClass( 'maybe-sticky' ),
   8658 					isSticky = headerElement.hasClass( 'is-sticky' ),
   8659 					isInView = headerElement.hasClass( 'is-in-view' ),
   8660 					isScrollingUp = ( -1 === scrollDirection );
   8661 
   8662 				// When scrolling down, gradually hide sticky header.
   8663 				if ( ! isScrollingUp ) {
   8664 					if ( isSticky ) {
   8665 						headerTop = scrollTop;
   8666 						headerElement
   8667 							.removeClass( 'is-sticky' )
   8668 							.css( {
   8669 								top:   headerTop + 'px',
   8670 								width: ''
   8671 							} );
   8672 					}
   8673 					if ( isInView && scrollTop > headerTop + headerHeight ) {
   8674 						headerElement.removeClass( 'is-in-view' );
   8675 						headerParent.css( 'padding-top', '' );
   8676 					}
   8677 					return;
   8678 				}
   8679 
   8680 				// Scrolling up.
   8681 				if ( ! maybeSticky && scrollTop >= headerHeight ) {
   8682 					maybeSticky = true;
   8683 					headerElement.addClass( 'maybe-sticky' );
   8684 				} else if ( 0 === scrollTop ) {
   8685 					// Reset header in base position.
   8686 					headerElement
   8687 						.removeClass( 'maybe-sticky is-in-view is-sticky' )
   8688 						.css( {
   8689 							top:   '',
   8690 							width: ''
   8691 						} );
   8692 					headerParent.css( 'padding-top', '' );
   8693 					return;
   8694 				}
   8695 
   8696 				if ( isInView && ! isSticky ) {
   8697 					// Header is in the view but is not yet sticky.
   8698 					if ( headerTop >= scrollTop ) {
   8699 						// Header is fully visible.
   8700 						headerElement
   8701 							.addClass( 'is-sticky' )
   8702 							.css( {
   8703 								top:   parentContainer.css( 'top' ),
   8704 								width: headerParent.outerWidth() + 'px'
   8705 							} );
   8706 					}
   8707 				} else if ( maybeSticky && ! isInView ) {
   8708 					// Header is out of the view.
   8709 					headerElement
   8710 						.addClass( 'is-in-view' )
   8711 						.css( 'top', ( scrollTop - headerHeight ) + 'px' );
   8712 					headerParent.css( 'padding-top', headerHeight + 'px' );
   8713 				}
   8714 			};
   8715 		}());
   8716 
   8717 		// Previewed device bindings. (The api.previewedDevice property
   8718 		// is how this Value was first introduced, but since it has moved to api.state.)
   8719 		api.previewedDevice = api.state( 'previewedDevice' );
   8720 
   8721 		// Set the default device.
   8722 		api.bind( 'ready', function() {
   8723 			_.find( api.settings.previewableDevices, function( value, key ) {
   8724 				if ( true === value['default'] ) {
   8725 					api.previewedDevice.set( key );
   8726 					return true;
   8727 				}
   8728 			} );
   8729 		} );
   8730 
   8731 		// Set the toggled device.
   8732 		footerActions.find( '.devices button' ).on( 'click', function( event ) {
   8733 			api.previewedDevice.set( $( event.currentTarget ).data( 'device' ) );
   8734 		});
   8735 
   8736 		// Bind device changes.
   8737 		api.previewedDevice.bind( function( newDevice ) {
   8738 			var overlay = $( '.wp-full-overlay' ),
   8739 				devices = '';
   8740 
   8741 			footerActions.find( '.devices button' )
   8742 				.removeClass( 'active' )
   8743 				.attr( 'aria-pressed', false );
   8744 
   8745 			footerActions.find( '.devices .preview-' + newDevice )
   8746 				.addClass( 'active' )
   8747 				.attr( 'aria-pressed', true );
   8748 
   8749 			$.each( api.settings.previewableDevices, function( device ) {
   8750 				devices += ' preview-' + device;
   8751 			} );
   8752 
   8753 			overlay
   8754 				.removeClass( devices )
   8755 				.addClass( 'preview-' + newDevice );
   8756 		} );
   8757 
   8758 		// Bind site title display to the corresponding field.
   8759 		if ( title.length ) {
   8760 			api( 'blogname', function( setting ) {
   8761 				var updateTitle = function() {
   8762 					var blogTitle = setting() || '';
   8763 					title.text( blogTitle.toString().trim() || api.l10n.untitledBlogName );
   8764 				};
   8765 				setting.bind( updateTitle );
   8766 				updateTitle();
   8767 			} );
   8768 		}
   8769 
   8770 		/*
   8771 		 * Create a postMessage connection with a parent frame,
   8772 		 * in case the Customizer frame was opened with the Customize loader.
   8773 		 *
   8774 		 * @see wp.customize.Loader
   8775 		 */
   8776 		parent = new api.Messenger({
   8777 			url: api.settings.url.parent,
   8778 			channel: 'loader'
   8779 		});
   8780 
   8781 		// Handle exiting of Customizer.
   8782 		(function() {
   8783 			var isInsideIframe = false;
   8784 
   8785 			function isCleanState() {
   8786 				var defaultChangesetStatus;
   8787 
   8788 				/*
   8789 				 * Handle special case of previewing theme switch since some settings (for nav menus and widgets)
   8790 				 * are pre-dirty and non-active themes can only ever be auto-drafts.
   8791 				 */
   8792 				if ( ! api.state( 'activated' ).get() ) {
   8793 					return 0 === api._latestRevision;
   8794 				}
   8795 
   8796 				// Dirty if the changeset status has been changed but not saved yet.
   8797 				defaultChangesetStatus = api.state( 'changesetStatus' ).get();
   8798 				if ( '' === defaultChangesetStatus || 'auto-draft' === defaultChangesetStatus ) {
   8799 					defaultChangesetStatus = 'publish';
   8800 				}
   8801 				if ( api.state( 'selectedChangesetStatus' ).get() !== defaultChangesetStatus ) {
   8802 					return false;
   8803 				}
   8804 
   8805 				// Dirty if scheduled but the changeset date hasn't been saved yet.
   8806 				if ( 'future' === api.state( 'selectedChangesetStatus' ).get() && api.state( 'selectedChangesetDate' ).get() !== api.state( 'changesetDate' ).get() ) {
   8807 					return false;
   8808 				}
   8809 
   8810 				return api.state( 'saved' ).get() && 'auto-draft' !== api.state( 'changesetStatus' ).get();
   8811 			}
   8812 
   8813 			/*
   8814 			 * If we receive a 'back' event, we're inside an iframe.
   8815 			 * Send any clicks to the 'Return' link to the parent page.
   8816 			 */
   8817 			parent.bind( 'back', function() {
   8818 				isInsideIframe = true;
   8819 			});
   8820 
   8821 			function startPromptingBeforeUnload() {
   8822 				api.unbind( 'change', startPromptingBeforeUnload );
   8823 				api.state( 'selectedChangesetStatus' ).unbind( startPromptingBeforeUnload );
   8824 				api.state( 'selectedChangesetDate' ).unbind( startPromptingBeforeUnload );
   8825 
   8826 				// Prompt user with AYS dialog if leaving the Customizer with unsaved changes.
   8827 				$( window ).on( 'beforeunload.customize-confirm', function() {
   8828 					if ( ! isCleanState() && ! api.state( 'changesetLocked' ).get() ) {
   8829 						setTimeout( function() {
   8830 							overlay.removeClass( 'customize-loading' );
   8831 						}, 1 );
   8832 						return api.l10n.saveAlert;
   8833 					}
   8834 				});
   8835 			}
   8836 			api.bind( 'change', startPromptingBeforeUnload );
   8837 			api.state( 'selectedChangesetStatus' ).bind( startPromptingBeforeUnload );
   8838 			api.state( 'selectedChangesetDate' ).bind( startPromptingBeforeUnload );
   8839 
   8840 			function requestClose() {
   8841 				var clearedToClose = $.Deferred(), dismissAutoSave = false, dismissLock = false;
   8842 
   8843 				if ( isCleanState() ) {
   8844 					dismissLock = true;
   8845 				} else if ( confirm( api.l10n.saveAlert ) ) {
   8846 
   8847 					dismissLock = true;
   8848 
   8849 					// Mark all settings as clean to prevent another call to requestChangesetUpdate.
   8850 					api.each( function( setting ) {
   8851 						setting._dirty = false;
   8852 					});
   8853 					$( document ).off( 'visibilitychange.wp-customize-changeset-update' );
   8854 					$( window ).off( 'beforeunload.wp-customize-changeset-update' );
   8855 
   8856 					closeBtn.css( 'cursor', 'progress' );
   8857 					if ( '' !== api.state( 'changesetStatus' ).get() ) {
   8858 						dismissAutoSave = true;
   8859 					}
   8860 				} else {
   8861 					clearedToClose.reject();
   8862 				}
   8863 
   8864 				if ( dismissLock || dismissAutoSave ) {
   8865 					wp.ajax.send( 'customize_dismiss_autosave_or_lock', {
   8866 						timeout: 500, // Don't wait too long.
   8867 						data: {
   8868 							wp_customize: 'on',
   8869 							customize_theme: api.settings.theme.stylesheet,
   8870 							customize_changeset_uuid: api.settings.changeset.uuid,
   8871 							nonce: api.settings.nonce.dismiss_autosave_or_lock,
   8872 							dismiss_autosave: dismissAutoSave,
   8873 							dismiss_lock: dismissLock
   8874 						}
   8875 					} ).always( function() {
   8876 						clearedToClose.resolve();
   8877 					} );
   8878 				}
   8879 
   8880 				return clearedToClose.promise();
   8881 			}
   8882 
   8883 			parent.bind( 'confirm-close', function() {
   8884 				requestClose().done( function() {
   8885 					parent.send( 'confirmed-close', true );
   8886 				} ).fail( function() {
   8887 					parent.send( 'confirmed-close', false );
   8888 				} );
   8889 			} );
   8890 
   8891 			closeBtn.on( 'click.customize-controls-close', function( event ) {
   8892 				event.preventDefault();
   8893 				if ( isInsideIframe ) {
   8894 					parent.send( 'close' ); // See confirm-close logic above.
   8895 				} else {
   8896 					requestClose().done( function() {
   8897 						$( window ).off( 'beforeunload.customize-confirm' );
   8898 						window.location.href = closeBtn.prop( 'href' );
   8899 					} );
   8900 				}
   8901 			});
   8902 		})();
   8903 
   8904 		// Pass events through to the parent.
   8905 		$.each( [ 'saved', 'change' ], function ( i, event ) {
   8906 			api.bind( event, function() {
   8907 				parent.send( event );
   8908 			});
   8909 		} );
   8910 
   8911 		// Pass titles to the parent.
   8912 		api.bind( 'title', function( newTitle ) {
   8913 			parent.send( 'title', newTitle );
   8914 		});
   8915 
   8916 		if ( api.settings.changeset.branching ) {
   8917 			parent.send( 'changeset-uuid', api.settings.changeset.uuid );
   8918 		}
   8919 
   8920 		// Initialize the connection with the parent frame.
   8921 		parent.send( 'ready' );
   8922 
   8923 		// Control visibility for default controls.
   8924 		$.each({
   8925 			'background_image': {
   8926 				controls: [ 'background_preset', 'background_position', 'background_size', 'background_repeat', 'background_attachment' ],
   8927 				callback: function( to ) { return !! to; }
   8928 			},
   8929 			'show_on_front': {
   8930 				controls: [ 'page_on_front', 'page_for_posts' ],
   8931 				callback: function( to ) { return 'page' === to; }
   8932 			},
   8933 			'header_textcolor': {
   8934 				controls: [ 'header_textcolor' ],
   8935 				callback: function( to ) { return 'blank' !== to; }
   8936 			}
   8937 		}, function( settingId, o ) {
   8938 			api( settingId, function( setting ) {
   8939 				$.each( o.controls, function( i, controlId ) {
   8940 					api.control( controlId, function( control ) {
   8941 						var visibility = function( to ) {
   8942 							control.container.toggle( o.callback( to ) );
   8943 						};
   8944 
   8945 						visibility( setting.get() );
   8946 						setting.bind( visibility );
   8947 					});
   8948 				});
   8949 			});
   8950 		});
   8951 
   8952 		api.control( 'background_preset', function( control ) {
   8953 			var visibility, defaultValues, values, toggleVisibility, updateSettings, preset;
   8954 
   8955 			visibility = { // position, size, repeat, attachment.
   8956 				'default': [ false, false, false, false ],
   8957 				'fill': [ true, false, false, false ],
   8958 				'fit': [ true, false, true, false ],
   8959 				'repeat': [ true, false, false, true ],
   8960 				'custom': [ true, true, true, true ]
   8961 			};
   8962 
   8963 			defaultValues = [
   8964 				_wpCustomizeBackground.defaults['default-position-x'],
   8965 				_wpCustomizeBackground.defaults['default-position-y'],
   8966 				_wpCustomizeBackground.defaults['default-size'],
   8967 				_wpCustomizeBackground.defaults['default-repeat'],
   8968 				_wpCustomizeBackground.defaults['default-attachment']
   8969 			];
   8970 
   8971 			values = { // position_x, position_y, size, repeat, attachment.
   8972 				'default': defaultValues,
   8973 				'fill': [ 'left', 'top', 'cover', 'no-repeat', 'fixed' ],
   8974 				'fit': [ 'left', 'top', 'contain', 'no-repeat', 'fixed' ],
   8975 				'repeat': [ 'left', 'top', 'auto', 'repeat', 'scroll' ]
   8976 			};
   8977 
   8978 			// @todo These should actually toggle the active state,
   8979 			// but without the preview overriding the state in data.activeControls.
   8980 			toggleVisibility = function( preset ) {
   8981 				_.each( [ 'background_position', 'background_size', 'background_repeat', 'background_attachment' ], function( controlId, i ) {
   8982 					var control = api.control( controlId );
   8983 					if ( control ) {
   8984 						control.container.toggle( visibility[ preset ][ i ] );
   8985 					}
   8986 				} );
   8987 			};
   8988 
   8989 			updateSettings = function( preset ) {
   8990 				_.each( [ 'background_position_x', 'background_position_y', 'background_size', 'background_repeat', 'background_attachment' ], function( settingId, i ) {
   8991 					var setting = api( settingId );
   8992 					if ( setting ) {
   8993 						setting.set( values[ preset ][ i ] );
   8994 					}
   8995 				} );
   8996 			};
   8997 
   8998 			preset = control.setting.get();
   8999 			toggleVisibility( preset );
   9000 
   9001 			control.setting.bind( 'change', function( preset ) {
   9002 				toggleVisibility( preset );
   9003 				if ( 'custom' !== preset ) {
   9004 					updateSettings( preset );
   9005 				}
   9006 			} );
   9007 		} );
   9008 
   9009 		api.control( 'background_repeat', function( control ) {
   9010 			control.elements[0].unsync( api( 'background_repeat' ) );
   9011 
   9012 			control.element = new api.Element( control.container.find( 'input' ) );
   9013 			control.element.set( 'no-repeat' !== control.setting() );
   9014 
   9015 			control.element.bind( function( to ) {
   9016 				control.setting.set( to ? 'repeat' : 'no-repeat' );
   9017 			} );
   9018 
   9019 			control.setting.bind( function( to ) {
   9020 				control.element.set( 'no-repeat' !== to );
   9021 			} );
   9022 		} );
   9023 
   9024 		api.control( 'background_attachment', function( control ) {
   9025 			control.elements[0].unsync( api( 'background_attachment' ) );
   9026 
   9027 			control.element = new api.Element( control.container.find( 'input' ) );
   9028 			control.element.set( 'fixed' !== control.setting() );
   9029 
   9030 			control.element.bind( function( to ) {
   9031 				control.setting.set( to ? 'scroll' : 'fixed' );
   9032 			} );
   9033 
   9034 			control.setting.bind( function( to ) {
   9035 				control.element.set( 'fixed' !== to );
   9036 			} );
   9037 		} );
   9038 
   9039 		// Juggle the two controls that use header_textcolor.
   9040 		api.control( 'display_header_text', function( control ) {
   9041 			var last = '';
   9042 
   9043 			control.elements[0].unsync( api( 'header_textcolor' ) );
   9044 
   9045 			control.element = new api.Element( control.container.find('input') );
   9046 			control.element.set( 'blank' !== control.setting() );
   9047 
   9048 			control.element.bind( function( to ) {
   9049 				if ( ! to ) {
   9050 					last = api( 'header_textcolor' ).get();
   9051 				}
   9052 
   9053 				control.setting.set( to ? last : 'blank' );
   9054 			});
   9055 
   9056 			control.setting.bind( function( to ) {
   9057 				control.element.set( 'blank' !== to );
   9058 			});
   9059 		});
   9060 
   9061 		// Add behaviors to the static front page controls.
   9062 		api( 'show_on_front', 'page_on_front', 'page_for_posts', function( showOnFront, pageOnFront, pageForPosts ) {
   9063 			var handleChange = function() {
   9064 				var setting = this, pageOnFrontId, pageForPostsId, errorCode = 'show_on_front_page_collision';
   9065 				pageOnFrontId = parseInt( pageOnFront(), 10 );
   9066 				pageForPostsId = parseInt( pageForPosts(), 10 );
   9067 
   9068 				if ( 'page' === showOnFront() ) {
   9069 
   9070 					// Change previewed URL to the homepage when changing the page_on_front.
   9071 					if ( setting === pageOnFront && pageOnFrontId > 0 ) {
   9072 						api.previewer.previewUrl.set( api.settings.url.home );
   9073 					}
   9074 
   9075 					// Change the previewed URL to the selected page when changing the page_for_posts.
   9076 					if ( setting === pageForPosts && pageForPostsId > 0 ) {
   9077 						api.previewer.previewUrl.set( api.settings.url.home + '?page_id=' + pageForPostsId );
   9078 					}
   9079 				}
   9080 
   9081 				// Toggle notification when the homepage and posts page are both set and the same.
   9082 				if ( 'page' === showOnFront() && pageOnFrontId && pageForPostsId && pageOnFrontId === pageForPostsId ) {
   9083 					showOnFront.notifications.add( new api.Notification( errorCode, {
   9084 						type: 'error',
   9085 						message: api.l10n.pageOnFrontError
   9086 					} ) );
   9087 				} else {
   9088 					showOnFront.notifications.remove( errorCode );
   9089 				}
   9090 			};
   9091 			showOnFront.bind( handleChange );
   9092 			pageOnFront.bind( handleChange );
   9093 			pageForPosts.bind( handleChange );
   9094 			handleChange.call( showOnFront, showOnFront() ); // Make sure initial notification is added after loading existing changeset.
   9095 
   9096 			// Move notifications container to the bottom.
   9097 			api.control( 'show_on_front', function( showOnFrontControl ) {
   9098 				showOnFrontControl.deferred.embedded.done( function() {
   9099 					showOnFrontControl.container.append( showOnFrontControl.getNotificationsContainerElement() );
   9100 				});
   9101 			});
   9102 		});
   9103 
   9104 		// Add code editor for Custom CSS.
   9105 		(function() {
   9106 			var sectionReady = $.Deferred();
   9107 
   9108 			api.section( 'custom_css', function( section ) {
   9109 				section.deferred.embedded.done( function() {
   9110 					if ( section.expanded() ) {
   9111 						sectionReady.resolve( section );
   9112 					} else {
   9113 						section.expanded.bind( function( isExpanded ) {
   9114 							if ( isExpanded ) {
   9115 								sectionReady.resolve( section );
   9116 							}
   9117 						} );
   9118 					}
   9119 				});
   9120 			});
   9121 
   9122 			// Set up the section description behaviors.
   9123 			sectionReady.done( function setupSectionDescription( section ) {
   9124 				var control = api.control( 'custom_css' );
   9125 
   9126 				// Hide redundant label for visual users.
   9127 				control.container.find( '.customize-control-title:first' ).addClass( 'screen-reader-text' );
   9128 
   9129 				// Close the section description when clicking the close button.
   9130 				section.container.find( '.section-description-buttons .section-description-close' ).on( 'click', function() {
   9131 					section.container.find( '.section-meta .customize-section-description:first' )
   9132 						.removeClass( 'open' )
   9133 						.slideUp();
   9134 
   9135 					section.container.find( '.customize-help-toggle' )
   9136 						.attr( 'aria-expanded', 'false' )
   9137 						.focus(); // Avoid focus loss.
   9138 				});
   9139 
   9140 				// Reveal help text if setting is empty.
   9141 				if ( control && ! control.setting.get() ) {
   9142 					section.container.find( '.section-meta .customize-section-description:first' )
   9143 						.addClass( 'open' )
   9144 						.show()
   9145 						.trigger( 'toggled' );
   9146 
   9147 					section.container.find( '.customize-help-toggle' ).attr( 'aria-expanded', 'true' );
   9148 				}
   9149 			});
   9150 		})();
   9151 
   9152 		// Toggle visibility of Header Video notice when active state change.
   9153 		api.control( 'header_video', function( headerVideoControl ) {
   9154 			headerVideoControl.deferred.embedded.done( function() {
   9155 				var toggleNotice = function() {
   9156 					var section = api.section( headerVideoControl.section() ), noticeCode = 'video_header_not_available';
   9157 					if ( ! section ) {
   9158 						return;
   9159 					}
   9160 					if ( headerVideoControl.active.get() ) {
   9161 						section.notifications.remove( noticeCode );
   9162 					} else {
   9163 						section.notifications.add( new api.Notification( noticeCode, {
   9164 							type: 'info',
   9165 							message: api.l10n.videoHeaderNotice
   9166 						} ) );
   9167 					}
   9168 				};
   9169 				toggleNotice();
   9170 				headerVideoControl.active.bind( toggleNotice );
   9171 			} );
   9172 		} );
   9173 
   9174 		// Update the setting validities.
   9175 		api.previewer.bind( 'selective-refresh-setting-validities', function handleSelectiveRefreshedSettingValidities( settingValidities ) {
   9176 			api._handleSettingValidities( {
   9177 				settingValidities: settingValidities,
   9178 				focusInvalidControl: false
   9179 			} );
   9180 		} );
   9181 
   9182 		// Focus on the control that is associated with the given setting.
   9183 		api.previewer.bind( 'focus-control-for-setting', function( settingId ) {
   9184 			var matchedControls = [];
   9185 			api.control.each( function( control ) {
   9186 				var settingIds = _.pluck( control.settings, 'id' );
   9187 				if ( -1 !== _.indexOf( settingIds, settingId ) ) {
   9188 					matchedControls.push( control );
   9189 				}
   9190 			} );
   9191 
   9192 			// Focus on the matched control with the lowest priority (appearing higher).
   9193 			if ( matchedControls.length ) {
   9194 				matchedControls.sort( function( a, b ) {
   9195 					return a.priority() - b.priority();
   9196 				} );
   9197 				matchedControls[0].focus();
   9198 			}
   9199 		} );
   9200 
   9201 		// Refresh the preview when it requests.
   9202 		api.previewer.bind( 'refresh', function() {
   9203 			api.previewer.refresh();
   9204 		});
   9205 
   9206 		// Update the edit shortcut visibility state.
   9207 		api.state( 'paneVisible' ).bind( function( isPaneVisible ) {
   9208 			var isMobileScreen;
   9209 			if ( window.matchMedia ) {
   9210 				isMobileScreen = window.matchMedia( 'screen and ( max-width: 640px )' ).matches;
   9211 			} else {
   9212 				isMobileScreen = $( window ).width() <= 640;
   9213 			}
   9214 			api.state( 'editShortcutVisibility' ).set( isPaneVisible || isMobileScreen ? 'visible' : 'hidden' );
   9215 		} );
   9216 		if ( window.matchMedia ) {
   9217 			window.matchMedia( 'screen and ( max-width: 640px )' ).addListener( function() {
   9218 				var state = api.state( 'paneVisible' );
   9219 				state.callbacks.fireWith( state, [ state.get(), state.get() ] );
   9220 			} );
   9221 		}
   9222 		api.previewer.bind( 'edit-shortcut-visibility', function( visibility ) {
   9223 			api.state( 'editShortcutVisibility' ).set( visibility );
   9224 		} );
   9225 		api.state( 'editShortcutVisibility' ).bind( function( visibility ) {
   9226 			api.previewer.send( 'edit-shortcut-visibility', visibility );
   9227 		} );
   9228 
   9229 		// Autosave changeset.
   9230 		function startAutosaving() {
   9231 			var timeoutId, updateChangesetWithReschedule, scheduleChangesetUpdate, updatePending = false;
   9232 
   9233 			api.unbind( 'change', startAutosaving ); // Ensure startAutosaving only fires once.
   9234 
   9235 			function onChangeSaved( isSaved ) {
   9236 				if ( ! isSaved && ! api.settings.changeset.autosaved ) {
   9237 					api.settings.changeset.autosaved = true; // Once a change is made then autosaving kicks in.
   9238 					api.previewer.send( 'autosaving' );
   9239 				}
   9240 			}
   9241 			api.state( 'saved' ).bind( onChangeSaved );
   9242 			onChangeSaved( api.state( 'saved' ).get() );
   9243 
   9244 			/**
   9245 			 * Request changeset update and then re-schedule the next changeset update time.
   9246 			 *
   9247 			 * @since 4.7.0
   9248 			 * @private
   9249 			 */
   9250 			updateChangesetWithReschedule = function() {
   9251 				if ( ! updatePending ) {
   9252 					updatePending = true;
   9253 					api.requestChangesetUpdate( {}, { autosave: true } ).always( function() {
   9254 						updatePending = false;
   9255 					} );
   9256 				}
   9257 				scheduleChangesetUpdate();
   9258 			};
   9259 
   9260 			/**
   9261 			 * Schedule changeset update.
   9262 			 *
   9263 			 * @since 4.7.0
   9264 			 * @private
   9265 			 */
   9266 			scheduleChangesetUpdate = function() {
   9267 				clearTimeout( timeoutId );
   9268 				timeoutId = setTimeout( function() {
   9269 					updateChangesetWithReschedule();
   9270 				}, api.settings.timeouts.changesetAutoSave );
   9271 			};
   9272 
   9273 			// Start auto-save interval for updating changeset.
   9274 			scheduleChangesetUpdate();
   9275 
   9276 			// Save changeset when focus removed from window.
   9277 			$( document ).on( 'visibilitychange.wp-customize-changeset-update', function() {
   9278 				if ( document.hidden ) {
   9279 					updateChangesetWithReschedule();
   9280 				}
   9281 			} );
   9282 
   9283 			// Save changeset before unloading window.
   9284 			$( window ).on( 'beforeunload.wp-customize-changeset-update', function() {
   9285 				updateChangesetWithReschedule();
   9286 			} );
   9287 		}
   9288 		api.bind( 'change', startAutosaving );
   9289 
   9290 		// Make sure TinyMCE dialogs appear above Customizer UI.
   9291 		$( document ).one( 'tinymce-editor-setup', function() {
   9292 			if ( window.tinymce.ui.FloatPanel && ( ! window.tinymce.ui.FloatPanel.zIndex || window.tinymce.ui.FloatPanel.zIndex < 500001 ) ) {
   9293 				window.tinymce.ui.FloatPanel.zIndex = 500001;
   9294 			}
   9295 		} );
   9296 
   9297 		body.addClass( 'ready' );
   9298 		api.trigger( 'ready' );
   9299 	});
   9300 
   9301 })( wp, jQuery );