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