wp/wp-admin/js/customize-controls.js
changeset 5 5e2f62d02dcd
parent 0 d970ebf37754
child 7 cf61fcea0001
equal deleted inserted replaced
4:346c88efed21 5:5e2f62d02dcd
       
     1 /* globals _wpCustomizeHeader, _wpCustomizeBackground, _wpMediaViewsL10n, MediaElementPlayer */
     1 (function( exports, $ ){
     2 (function( exports, $ ){
     2 	var api = wp.customize;
     3 	var Container, focus, api = wp.customize;
     3 
     4 
     4 	/*
     5 	/**
       
     6 	 * @class
       
     7 	 * @augments wp.customize.Value
       
     8 	 * @augments wp.customize.Class
       
     9 	 *
     5 	 * @param options
    10 	 * @param options
     6 	 * - previewer - The Previewer instance to sync with.
    11 	 * - previewer - The Previewer instance to sync with.
     7 	 * - transport - The transport to use for previewing. Supports 'refresh' and 'postMessage'.
    12 	 * - transport - The transport to use for previewing. Supports 'refresh' and 'postMessage'.
     8 	 */
    13 	 */
     9 	api.Setting = api.Value.extend({
    14 	api.Setting = api.Value.extend({
    10 		initialize: function( id, value, options ) {
    15 		initialize: function( id, value, options ) {
    11 			var element;
       
    12 
       
    13 			api.Value.prototype.initialize.call( this, value, options );
    16 			api.Value.prototype.initialize.call( this, value, options );
    14 
    17 
    15 			this.id = id;
    18 			this.id = id;
    16 			this.transport = this.transport || 'refresh';
    19 			this.transport = this.transport || 'refresh';
       
    20 			this._dirty = options.dirty || false;
    17 
    21 
    18 			this.bind( this.preview );
    22 			this.bind( this.preview );
    19 		},
    23 		},
    20 		preview: function() {
    24 		preview: function() {
    21 			switch ( this.transport ) {
    25 			switch ( this.transport ) {
    25 					return this.previewer.send( 'setting', [ this.id, this() ] );
    29 					return this.previewer.send( 'setting', [ this.id, this() ] );
    26 			}
    30 			}
    27 		}
    31 		}
    28 	});
    32 	});
    29 
    33 
       
    34 	/**
       
    35 	 * Utility function namespace
       
    36 	 */
       
    37 	api.utils = {};
       
    38 
       
    39 	/**
       
    40 	 * Watch all changes to Value properties, and bubble changes to parent Values instance
       
    41 	 *
       
    42 	 * @since 4.1.0
       
    43 	 *
       
    44 	 * @param {wp.customize.Class} instance
       
    45 	 * @param {Array}              properties  The names of the Value instances to watch.
       
    46 	 */
       
    47 	api.utils.bubbleChildValueChanges = function ( instance, properties ) {
       
    48 		$.each( properties, function ( i, key ) {
       
    49 			instance[ key ].bind( function ( to, from ) {
       
    50 				if ( instance.parent && to !== from ) {
       
    51 					instance.parent.trigger( 'change', instance );
       
    52 				}
       
    53 			} );
       
    54 		} );
       
    55 	};
       
    56 
       
    57 	/**
       
    58 	 * Expand a panel, section, or control and focus on the first focusable element.
       
    59 	 *
       
    60 	 * @since 4.1.0
       
    61 	 *
       
    62 	 * @param {Object}   [params]
       
    63 	 * @param {Callback} [params.completeCallback]
       
    64 	 */
       
    65 	focus = function ( params ) {
       
    66 		var construct, completeCallback, focus;
       
    67 		construct = this;
       
    68 		params = params || {};
       
    69 		focus = function () {
       
    70 			var focusContainer;
       
    71 			if ( construct.extended( api.Panel ) && construct.expanded() ) {
       
    72 				focusContainer = construct.container.find( '.control-panel-content:first' );
       
    73 			} else {
       
    74 				focusContainer = construct.container;
       
    75 			}
       
    76 			focusContainer.find( ':focusable:first' ).focus();
       
    77 			focusContainer[0].scrollIntoView( true );
       
    78 		};
       
    79 		if ( params.completeCallback ) {
       
    80 			completeCallback = params.completeCallback;
       
    81 			params.completeCallback = function () {
       
    82 				focus();
       
    83 				completeCallback();
       
    84 			};
       
    85 		} else {
       
    86 			params.completeCallback = focus;
       
    87 		}
       
    88 		if ( construct.expand ) {
       
    89 			construct.expand( params );
       
    90 		} else {
       
    91 			params.completeCallback();
       
    92 		}
       
    93 	};
       
    94 
       
    95 	/**
       
    96 	 * Stable sort for Panels, Sections, and Controls.
       
    97 	 *
       
    98 	 * If a.priority() === b.priority(), then sort by their respective params.instanceNumber.
       
    99 	 *
       
   100 	 * @since 4.1.0
       
   101 	 *
       
   102 	 * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} a
       
   103 	 * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} b
       
   104 	 * @returns {Number}
       
   105 	 */
       
   106 	api.utils.prioritySort = function ( a, b ) {
       
   107 		if ( a.priority() === b.priority() && typeof a.params.instanceNumber === 'number' && typeof b.params.instanceNumber === 'number' ) {
       
   108 			return a.params.instanceNumber - b.params.instanceNumber;
       
   109 		} else {
       
   110 			return a.priority() - b.priority();
       
   111 		}
       
   112 	};
       
   113 
       
   114 	/**
       
   115 	 * Return whether the supplied Event object is for a keydown event but not the Enter key.
       
   116 	 *
       
   117 	 * @since 4.1.0
       
   118 	 *
       
   119 	 * @param {jQuery.Event} event
       
   120 	 * @returns {boolean}
       
   121 	 */
       
   122 	api.utils.isKeydownButNotEnterEvent = function ( event ) {
       
   123 		return ( 'keydown' === event.type && 13 !== event.which );
       
   124 	};
       
   125 
       
   126 	/**
       
   127 	 * Return whether the two lists of elements are the same and are in the same order.
       
   128 	 *
       
   129 	 * @since 4.1.0
       
   130 	 *
       
   131 	 * @param {Array|jQuery} listA
       
   132 	 * @param {Array|jQuery} listB
       
   133 	 * @returns {boolean}
       
   134 	 */
       
   135 	api.utils.areElementListsEqual = function ( listA, listB ) {
       
   136 		var equal = (
       
   137 			listA.length === listB.length && // if lists are different lengths, then naturally they are not equal
       
   138 			-1 === _.indexOf( _.map( // are there any false values in the list returned by map?
       
   139 				_.zip( listA, listB ), // pair up each element between the two lists
       
   140 				function ( pair ) {
       
   141 					return $( pair[0] ).is( pair[1] ); // compare to see if each pair are equal
       
   142 				}
       
   143 			), false ) // check for presence of false in map's return value
       
   144 		);
       
   145 		return equal;
       
   146 	};
       
   147 
       
   148 	/**
       
   149 	 * Base class for Panel and Section.
       
   150 	 *
       
   151 	 * @since 4.1.0
       
   152 	 *
       
   153 	 * @class
       
   154 	 * @augments wp.customize.Class
       
   155 	 */
       
   156 	Container = api.Class.extend({
       
   157 		defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
       
   158 		defaultExpandedArguments: { duration: 'fast', completeCallback: $.noop },
       
   159 
       
   160 		/**
       
   161 		 * @since 4.1.0
       
   162 		 *
       
   163 		 * @param {String} id
       
   164 		 * @param {Object} options
       
   165 		 */
       
   166 		initialize: function ( id, options ) {
       
   167 			var container = this;
       
   168 			container.id = id;
       
   169 			container.params = {};
       
   170 			$.extend( container, options || {} );
       
   171 			container.container = $( container.params.content );
       
   172 
       
   173 			container.deferred = {
       
   174 				embedded: new $.Deferred()
       
   175 			};
       
   176 			container.priority = new api.Value();
       
   177 			container.active = new api.Value();
       
   178 			container.activeArgumentsQueue = [];
       
   179 			container.expanded = new api.Value();
       
   180 			container.expandedArgumentsQueue = [];
       
   181 
       
   182 			container.active.bind( function ( active ) {
       
   183 				var args = container.activeArgumentsQueue.shift();
       
   184 				args = $.extend( {}, container.defaultActiveArguments, args );
       
   185 				active = ( active && container.isContextuallyActive() );
       
   186 				container.onChangeActive( active, args );
       
   187 			});
       
   188 			container.expanded.bind( function ( expanded ) {
       
   189 				var args = container.expandedArgumentsQueue.shift();
       
   190 				args = $.extend( {}, container.defaultExpandedArguments, args );
       
   191 				container.onChangeExpanded( expanded, args );
       
   192 			});
       
   193 
       
   194 			container.attachEvents();
       
   195 
       
   196 			api.utils.bubbleChildValueChanges( container, [ 'priority', 'active' ] );
       
   197 
       
   198 			container.priority.set( isNaN( container.params.priority ) ? 100 : container.params.priority );
       
   199 			container.active.set( container.params.active );
       
   200 			container.expanded.set( false );
       
   201 		},
       
   202 
       
   203 		/**
       
   204 		 * @since 4.1.0
       
   205 		 *
       
   206 		 * @abstract
       
   207 		 */
       
   208 		ready: function() {},
       
   209 
       
   210 		/**
       
   211 		 * Get the child models associated with this parent, sorting them by their priority Value.
       
   212 		 *
       
   213 		 * @since 4.1.0
       
   214 		 *
       
   215 		 * @param {String} parentType
       
   216 		 * @param {String} childType
       
   217 		 * @returns {Array}
       
   218 		 */
       
   219 		_children: function ( parentType, childType ) {
       
   220 			var parent = this,
       
   221 				children = [];
       
   222 			api[ childType ].each( function ( child ) {
       
   223 				if ( child[ parentType ].get() === parent.id ) {
       
   224 					children.push( child );
       
   225 				}
       
   226 			} );
       
   227 			children.sort( api.utils.prioritySort );
       
   228 			return children;
       
   229 		},
       
   230 
       
   231 		/**
       
   232 		 * To override by subclass, to return whether the container has active children.
       
   233 		 *
       
   234 		 * @since 4.1.0
       
   235 		 *
       
   236 		 * @abstract
       
   237 		 */
       
   238 		isContextuallyActive: function () {
       
   239 			throw new Error( 'Container.isContextuallyActive() must be overridden in a subclass.' );
       
   240 		},
       
   241 
       
   242 		/**
       
   243 		 * Handle changes to the active state.
       
   244 		 *
       
   245 		 * This does not change the active state, it merely handles the behavior
       
   246 		 * for when it does change.
       
   247 		 *
       
   248 		 * To override by subclass, update the container's UI to reflect the provided active state.
       
   249 		 *
       
   250 		 * @since 4.1.0
       
   251 		 *
       
   252 		 * @param {Boolean} active
       
   253 		 * @param {Object}  args
       
   254 		 * @param {Object}  args.duration
       
   255 		 * @param {Object}  args.completeCallback
       
   256 		 */
       
   257 		onChangeActive: function ( active, args ) {
       
   258 			var duration = ( 'resolved' === api.previewer.deferred.active.state() ? args.duration : 0 );
       
   259 			if ( ! $.contains( document, this.container ) ) {
       
   260 				// jQuery.fn.slideUp is not hiding an element if it is not in the DOM
       
   261 				this.container.toggle( active );
       
   262 				if ( args.completeCallback ) {
       
   263 					args.completeCallback();
       
   264 				}
       
   265 			} else if ( active ) {
       
   266 				this.container.stop( true, true ).slideDown( duration, args.completeCallback );
       
   267 			} else {
       
   268 				this.container.stop( true, true ).slideUp( duration, args.completeCallback );
       
   269 			}
       
   270 		},
       
   271 
       
   272 		/**
       
   273 		 * @since 4.1.0
       
   274 		 *
       
   275 		 * @params {Boolean} active
       
   276 		 * @param {Object}   [params]
       
   277 		 * @returns {Boolean} false if state already applied
       
   278 		 */
       
   279 		_toggleActive: function ( active, params ) {
       
   280 			var self = this;
       
   281 			params = params || {};
       
   282 			if ( ( active && this.active.get() ) || ( ! active && ! this.active.get() ) ) {
       
   283 				params.unchanged = true;
       
   284 				self.onChangeActive( self.active.get(), params );
       
   285 				return false;
       
   286 			} else {
       
   287 				params.unchanged = false;
       
   288 				this.activeArgumentsQueue.push( params );
       
   289 				this.active.set( active );
       
   290 				return true;
       
   291 			}
       
   292 		},
       
   293 
       
   294 		/**
       
   295 		 * @param {Object} [params]
       
   296 		 * @returns {Boolean} false if already active
       
   297 		 */
       
   298 		activate: function ( params ) {
       
   299 			return this._toggleActive( true, params );
       
   300 		},
       
   301 
       
   302 		/**
       
   303 		 * @param {Object} [params]
       
   304 		 * @returns {Boolean} false if already inactive
       
   305 		 */
       
   306 		deactivate: function ( params ) {
       
   307 			return this._toggleActive( false, params );
       
   308 		},
       
   309 
       
   310 		/**
       
   311 		 * To override by subclass, update the container's UI to reflect the provided active state.
       
   312 		 * @abstract
       
   313 		 */
       
   314 		onChangeExpanded: function () {
       
   315 			throw new Error( 'Must override with subclass.' );
       
   316 		},
       
   317 
       
   318 		/**
       
   319 		 * @param {Boolean} expanded
       
   320 		 * @param {Object} [params]
       
   321 		 * @returns {Boolean} false if state already applied
       
   322 		 */
       
   323 		_toggleExpanded: function ( expanded, params ) {
       
   324 			var self = this;
       
   325 			params = params || {};
       
   326 			var section = this, previousCompleteCallback = params.completeCallback;
       
   327 			params.completeCallback = function () {
       
   328 				if ( previousCompleteCallback ) {
       
   329 					previousCompleteCallback.apply( section, arguments );
       
   330 				}
       
   331 				if ( expanded ) {
       
   332 					section.container.trigger( 'expanded' );
       
   333 				} else {
       
   334 					section.container.trigger( 'collapsed' );
       
   335 				}
       
   336 			};
       
   337 			if ( ( expanded && this.expanded.get() ) || ( ! expanded && ! this.expanded.get() ) ) {
       
   338 				params.unchanged = true;
       
   339 				self.onChangeExpanded( self.expanded.get(), params );
       
   340 				return false;
       
   341 			} else {
       
   342 				params.unchanged = false;
       
   343 				this.expandedArgumentsQueue.push( params );
       
   344 				this.expanded.set( expanded );
       
   345 				return true;
       
   346 			}
       
   347 		},
       
   348 
       
   349 		/**
       
   350 		 * @param {Object} [params]
       
   351 		 * @returns {Boolean} false if already expanded
       
   352 		 */
       
   353 		expand: function ( params ) {
       
   354 			return this._toggleExpanded( true, params );
       
   355 		},
       
   356 
       
   357 		/**
       
   358 		 * @param {Object} [params]
       
   359 		 * @returns {Boolean} false if already collapsed
       
   360 		 */
       
   361 		collapse: function ( params ) {
       
   362 			return this._toggleExpanded( false, params );
       
   363 		},
       
   364 
       
   365 		/**
       
   366 		 * Bring the container into view and then expand this and bring it into view
       
   367 		 * @param {Object} [params]
       
   368 		 */
       
   369 		focus: focus
       
   370 	});
       
   371 
       
   372 	/**
       
   373 	 * @since 4.1.0
       
   374 	 *
       
   375 	 * @class
       
   376 	 * @augments wp.customize.Class
       
   377 	 */
       
   378 	api.Section = Container.extend({
       
   379 
       
   380 		/**
       
   381 		 * @since 4.1.0
       
   382 		 *
       
   383 		 * @param {String} id
       
   384 		 * @param {Array}  options
       
   385 		 */
       
   386 		initialize: function ( id, options ) {
       
   387 			var section = this;
       
   388 			Container.prototype.initialize.call( section, id, options );
       
   389 
       
   390 			section.id = id;
       
   391 			section.panel = new api.Value();
       
   392 			section.panel.bind( function ( id ) {
       
   393 				$( section.container ).toggleClass( 'control-subsection', !! id );
       
   394 			});
       
   395 			section.panel.set( section.params.panel || '' );
       
   396 			api.utils.bubbleChildValueChanges( section, [ 'panel' ] );
       
   397 
       
   398 			section.embed();
       
   399 			section.deferred.embedded.done( function () {
       
   400 				section.ready();
       
   401 			});
       
   402 		},
       
   403 
       
   404 		/**
       
   405 		 * Embed the container in the DOM when any parent panel is ready.
       
   406 		 *
       
   407 		 * @since 4.1.0
       
   408 		 */
       
   409 		embed: function () {
       
   410 			var section = this, inject;
       
   411 
       
   412 			// Watch for changes to the panel state
       
   413 			inject = function ( panelId ) {
       
   414 				var parentContainer;
       
   415 				if ( panelId ) {
       
   416 					// The panel has been supplied, so wait until the panel object is registered
       
   417 					api.panel( panelId, function ( panel ) {
       
   418 						// The panel has been registered, wait for it to become ready/initialized
       
   419 						panel.deferred.embedded.done( function () {
       
   420 							parentContainer = panel.container.find( 'ul:first' );
       
   421 							if ( ! section.container.parent().is( parentContainer ) ) {
       
   422 								parentContainer.append( section.container );
       
   423 							}
       
   424 							section.deferred.embedded.resolve();
       
   425 						});
       
   426 					} );
       
   427 				} else {
       
   428 					// 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
       
   430 					if ( ! section.container.parent().is( parentContainer ) ) {
       
   431 						parentContainer.append( section.container );
       
   432 					}
       
   433 					section.deferred.embedded.resolve();
       
   434 				}
       
   435 			};
       
   436 			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
       
   438 		},
       
   439 
       
   440 		/**
       
   441 		 * Add behaviors for the accordion section.
       
   442 		 *
       
   443 		 * @since 4.1.0
       
   444 		 */
       
   445 		attachEvents: function () {
       
   446 			var section = this;
       
   447 
       
   448 			// Expand/Collapse accordion sections on click.
       
   449 			section.container.find( '.accordion-section-title' ).on( 'click keydown', function( event ) {
       
   450 				if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
       
   451 					return;
       
   452 				}
       
   453 				event.preventDefault(); // Keep this AFTER the key filter above
       
   454 
       
   455 				if ( section.expanded() ) {
       
   456 					section.collapse();
       
   457 				} else {
       
   458 					section.expand();
       
   459 				}
       
   460 			});
       
   461 		},
       
   462 
       
   463 		/**
       
   464 		 * Return whether this section has any active controls.
       
   465 		 *
       
   466 		 * @since 4.1.0
       
   467 		 *
       
   468 		 * @returns {Boolean}
       
   469 		 */
       
   470 		isContextuallyActive: function () {
       
   471 			var section = this,
       
   472 				controls = section.controls(),
       
   473 				activeCount = 0;
       
   474 			_( controls ).each( function ( control ) {
       
   475 				if ( control.active() ) {
       
   476 					activeCount += 1;
       
   477 				}
       
   478 			} );
       
   479 			return ( activeCount !== 0 );
       
   480 		},
       
   481 
       
   482 		/**
       
   483 		 * Get the controls that are associated with this section, sorted by their priority Value.
       
   484 		 *
       
   485 		 * @since 4.1.0
       
   486 		 *
       
   487 		 * @returns {Array}
       
   488 		 */
       
   489 		controls: function () {
       
   490 			return this._children( 'section', 'control' );
       
   491 		},
       
   492 
       
   493 		/**
       
   494 		 * Update UI to reflect expanded state.
       
   495 		 *
       
   496 		 * @since 4.1.0
       
   497 		 *
       
   498 		 * @param {Boolean} expanded
       
   499 		 * @param {Object}  args
       
   500 		 */
       
   501 		onChangeExpanded: function ( expanded, args ) {
       
   502 			var section = this,
       
   503 				content = section.container.find( '.accordion-section-content' ),
       
   504 				expand;
       
   505 
       
   506 			if ( expanded ) {
       
   507 
       
   508 				if ( args.unchanged ) {
       
   509 					expand = args.completeCallback;
       
   510 				} else {
       
   511 					expand = function () {
       
   512 						content.stop().slideDown( args.duration, args.completeCallback );
       
   513 						section.container.addClass( 'open' );
       
   514 					};
       
   515 				}
       
   516 
       
   517 				if ( ! args.allowMultiple ) {
       
   518 					api.section.each( function ( otherSection ) {
       
   519 						if ( otherSection !== section ) {
       
   520 							otherSection.collapse( { duration: args.duration } );
       
   521 						}
       
   522 					});
       
   523 				}
       
   524 
       
   525 				if ( section.panel() ) {
       
   526 					api.panel( section.panel() ).expand({
       
   527 						duration: args.duration,
       
   528 						completeCallback: expand
       
   529 					});
       
   530 				} else {
       
   531 					expand();
       
   532 				}
       
   533 
       
   534 			} else {
       
   535 				section.container.removeClass( 'open' );
       
   536 				content.slideUp( args.duration, args.completeCallback );
       
   537 			}
       
   538 		}
       
   539 	});
       
   540 
       
   541 	/**
       
   542 	 * wp.customize.ThemesSection
       
   543 	 *
       
   544 	 * Custom section for themes that functions similarly to a backwards panel,
       
   545 	 * and also handles the theme-details view rendering and navigation.
       
   546 	 *
       
   547 	 * @constructor
       
   548 	 * @augments wp.customize.Section
       
   549 	 * @augments wp.customize.Container
       
   550 	 */
       
   551 	api.ThemesSection = api.Section.extend({
       
   552 		currentTheme: '',
       
   553 		overlay: '',
       
   554 		template: '',
       
   555 		screenshotQueue: null,
       
   556 		$window: $( window ),
       
   557 
       
   558 		/**
       
   559 		 * @since 4.2.0
       
   560 		 */
       
   561 		initialize: function () {
       
   562 			this.$customizeSidebar = $( '.wp-full-overlay-sidebar-content:first' );
       
   563 			return api.Section.prototype.initialize.apply( this, arguments );
       
   564 		},
       
   565 
       
   566 		/**
       
   567 		 * @since 4.2.0
       
   568 		 */
       
   569 		ready: function () {
       
   570 			var section = this;
       
   571 			section.overlay = section.container.find( '.theme-overlay' );
       
   572 			section.template = wp.template( 'customize-themes-details-view' );
       
   573 
       
   574 			// Bind global keyboard events.
       
   575 			$( 'body' ).on( 'keyup', function( event ) {
       
   576 				if ( ! section.overlay.find( '.theme-wrap' ).is( ':visible' ) ) {
       
   577 					return;
       
   578 				}
       
   579 
       
   580 				// Pressing the right arrow key fires a theme:next event
       
   581 				if ( 39 === event.keyCode ) {
       
   582 					section.nextTheme();
       
   583 				}
       
   584 
       
   585 				// Pressing the left arrow key fires a theme:previous event
       
   586 				if ( 37 === event.keyCode ) {
       
   587 					section.previousTheme();
       
   588 				}
       
   589 
       
   590 				// Pressing the escape key fires a theme:collapse event
       
   591 				if ( 27 === event.keyCode ) {
       
   592 					section.closeDetails();
       
   593 				}
       
   594 			});
       
   595 
       
   596 			_.bindAll( this, 'renderScreenshots' );
       
   597 		},
       
   598 
       
   599 		/**
       
   600 		 * Override Section.isContextuallyActive method.
       
   601 		 *
       
   602 		 * Ignore the active states' of the contained theme controls, and just
       
   603 		 * use the section's own active state instead. This ensures empty search
       
   604 		 * results for themes to cause the section to become inactive.
       
   605 		 *
       
   606 		 * @since 4.2.0
       
   607 		 *
       
   608 		 * @returns {Boolean}
       
   609 		 */
       
   610 		isContextuallyActive: function () {
       
   611 			return this.active();
       
   612 		},
       
   613 
       
   614 		/**
       
   615 		 * @since 4.2.0
       
   616 		 */
       
   617 		attachEvents: function () {
       
   618 			var section = this;
       
   619 
       
   620 			// Expand/Collapse section/panel.
       
   621 			section.container.find( '.change-theme, .customize-theme' ).on( 'click keydown', function( event ) {
       
   622 				if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
       
   623 					return;
       
   624 				}
       
   625 				event.preventDefault(); // Keep this AFTER the key filter above
       
   626 
       
   627 				if ( section.expanded() ) {
       
   628 					section.collapse();
       
   629 				} else {
       
   630 					section.expand();
       
   631 				}
       
   632 			});
       
   633 
       
   634 			// Theme navigation in details view.
       
   635 			section.container.on( 'click keydown', '.left', function( event ) {
       
   636 				if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
       
   637 					return;
       
   638 				}
       
   639 
       
   640 				event.preventDefault(); // Keep this AFTER the key filter above
       
   641 
       
   642 				section.previousTheme();
       
   643 			});
       
   644 
       
   645 			section.container.on( 'click keydown', '.right', function( event ) {
       
   646 				if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
       
   647 					return;
       
   648 				}
       
   649 
       
   650 				event.preventDefault(); // Keep this AFTER the key filter above
       
   651 
       
   652 				section.nextTheme();
       
   653 			});
       
   654 
       
   655 			section.container.on( 'click keydown', '.theme-backdrop, .close', function( event ) {
       
   656 				if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
       
   657 					return;
       
   658 				}
       
   659 
       
   660 				event.preventDefault(); // Keep this AFTER the key filter above
       
   661 
       
   662 				section.closeDetails();
       
   663 			});
       
   664 
       
   665 			var renderScreenshots = _.throttle( _.bind( section.renderScreenshots, this ), 100 );
       
   666 			section.container.on( 'input', '#themes-filter', function( event ) {
       
   667 				var count,
       
   668 					term = event.currentTarget.value.toLowerCase().trim().replace( '-', ' ' ),
       
   669 					controls = section.controls();
       
   670 
       
   671 				_.each( controls, function( control ) {
       
   672 					control.filter( term );
       
   673 				});
       
   674 
       
   675 				renderScreenshots();
       
   676 
       
   677 				// Update theme count.
       
   678 				count = section.container.find( 'li.customize-control:visible' ).length;
       
   679 				section.container.find( '.theme-count' ).text( count );
       
   680 			});
       
   681 
       
   682 			// Pre-load the first 3 theme screenshots.
       
   683 			api.bind( 'ready', function () {
       
   684 				_.each( section.controls().slice( 0, 3 ), function ( control ) {
       
   685 					var img, src = control.params.theme.screenshot[0];
       
   686 					if ( src ) {
       
   687 						img = new Image();
       
   688 						img.src = src;
       
   689 					}
       
   690 				});
       
   691 			});
       
   692 		},
       
   693 
       
   694 		/**
       
   695 		 * Update UI to reflect expanded state
       
   696 		 *
       
   697 		 * @since 4.2.0
       
   698 		 *
       
   699 		 * @param {Boolean}  expanded
       
   700 		 * @param {Object}   args
       
   701 		 * @param {Boolean}  args.unchanged
       
   702 		 * @param {Callback} args.completeCallback
       
   703 		 */
       
   704 		onChangeExpanded: function ( expanded, args ) {
       
   705 
       
   706 			// Immediately call the complete callback if there were no changes
       
   707 			if ( args.unchanged ) {
       
   708 				if ( args.completeCallback ) {
       
   709 					args.completeCallback();
       
   710 				}
       
   711 				return;
       
   712 			}
       
   713 
       
   714 			// Note: there is a second argument 'args' passed
       
   715 			var position, scroll,
       
   716 				panel = this,
       
   717 				section = panel.container.closest( '.accordion-section' ),
       
   718 				overlay = section.closest( '.wp-full-overlay' ),
       
   719 				container = section.closest( '.wp-full-overlay-sidebar-content' ),
       
   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 
       
   728 				// Collapse any sibling sections/panels
       
   729 				api.section.each( function ( otherSection ) {
       
   730 					if ( otherSection !== panel ) {
       
   731 						otherSection.collapse( { duration: args.duration } );
       
   732 					}
       
   733 				});
       
   734 				api.panel.each( function ( otherPanel ) {
       
   735 					otherPanel.collapse( { duration: 0 } );
       
   736 				});
       
   737 
       
   738 				content.show( 0, function() {
       
   739 					position = content.offset().top;
       
   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 ) {
       
   787 				return;
       
   788 			}
       
   789 
       
   790 			section.screenshotQueue = _.filter( section.screenshotQueue, function( control ) {
       
   791 				var $imageWrapper = control.container.find( '.theme-screenshot' ),
       
   792 					$image = $imageWrapper.find( 'img' );
       
   793 
       
   794 				if ( ! $image.length ) {
       
   795 					return false;
       
   796 				}
       
   797 
       
   798 				if ( $image.is( ':hidden' ) ) {
       
   799 					return true;
       
   800 				}
       
   801 
       
   802 				// Based on unveil.js.
       
   803 				var wt = section.$window.scrollTop(),
       
   804 					wb = wt + section.$window.height(),
       
   805 					et = $image.offset().top,
       
   806 					ih = $imageWrapper.height(),
       
   807 					eb = et + ih,
       
   808 					threshold = ih * 3,
       
   809 					inView = eb >= wt - threshold && et <= wb + threshold;
       
   810 
       
   811 				if ( inView ) {
       
   812 					control.container.trigger( 'render-screenshot' );
       
   813 				}
       
   814 
       
   815 				// If the image is in view return false so it's cleared from the queue.
       
   816 				return ! inView;
       
   817 			} );
       
   818 		},
       
   819 
       
   820 		/**
       
   821 		 * Advance the modal to the next theme.
       
   822 		 *
       
   823 		 * @since 4.2.0
       
   824 		 */
       
   825 		nextTheme: function () {
       
   826 			var section = this;
       
   827 			if ( section.getNextTheme() ) {
       
   828 				section.showDetails( section.getNextTheme(), function() {
       
   829 					section.overlay.find( '.right' ).focus();
       
   830 				} );
       
   831 			}
       
   832 		},
       
   833 
       
   834 		/**
       
   835 		 * Get the next theme model.
       
   836 		 *
       
   837 		 * @since 4.2.0
       
   838 		 */
       
   839 		getNextTheme: function () {
       
   840 			var control, next;
       
   841 			control = api.control( 'theme_' + this.currentTheme );
       
   842 			next = control.container.next( 'li.customize-control-theme' );
       
   843 			if ( ! next.length ) {
       
   844 				return false;
       
   845 			}
       
   846 			next = next[0].id.replace( 'customize-control-', '' );
       
   847 			control = api.control( next );
       
   848 
       
   849 			return control.params.theme;
       
   850 		},
       
   851 
       
   852 		/**
       
   853 		 * Advance the modal to the previous theme.
       
   854 		 *
       
   855 		 * @since 4.2.0
       
   856 		 */
       
   857 		previousTheme: function () {
       
   858 			var section = this;
       
   859 			if ( section.getPreviousTheme() ) {
       
   860 				section.showDetails( section.getPreviousTheme(), function() {
       
   861 					section.overlay.find( '.left' ).focus();
       
   862 				} );
       
   863 			}
       
   864 		},
       
   865 
       
   866 		/**
       
   867 		 * Get the previous theme model.
       
   868 		 *
       
   869 		 * @since 4.2.0
       
   870 		 */
       
   871 		getPreviousTheme: function () {
       
   872 			var control, previous;
       
   873 			control = api.control( 'theme_' + this.currentTheme );
       
   874 			previous = control.container.prev( 'li.customize-control-theme' );
       
   875 			if ( ! previous.length ) {
       
   876 				return false;
       
   877 			}
       
   878 			previous = previous[0].id.replace( 'customize-control-', '' );
       
   879 			control = api.control( previous );
       
   880 
       
   881 			return control.params.theme;
       
   882 		},
       
   883 
       
   884 		/**
       
   885 		 * Disable buttons when we're viewing the first or last theme.
       
   886 		 *
       
   887 		 * @since 4.2.0
       
   888 		 */
       
   889 		updateLimits: function () {
       
   890 			if ( ! this.getNextTheme() ) {
       
   891 				this.overlay.find( '.right' ).addClass( 'disabled' );
       
   892 			}
       
   893 			if ( ! this.getPreviousTheme() ) {
       
   894 				this.overlay.find( '.left' ).addClass( 'disabled' );
       
   895 			}
       
   896 		},
       
   897 
       
   898 		/**
       
   899 		 * Render & show the theme details for a given theme model.
       
   900 		 *
       
   901 		 * @since 4.2.0
       
   902 		 *
       
   903 		 * @param {Object}   theme
       
   904 		 */
       
   905 		showDetails: function ( theme, callback ) {
       
   906 			var section = this;
       
   907 			callback = callback || function(){};
       
   908 			section.currentTheme = theme.id;
       
   909 			section.overlay.html( section.template( theme ) )
       
   910 				.fadeIn( 'fast' )
       
   911 				.focus();
       
   912 			$( 'body' ).addClass( 'modal-open' );
       
   913 			section.containFocus( section.overlay );
       
   914 			section.updateLimits();
       
   915 			callback();
       
   916 		},
       
   917 
       
   918 		/**
       
   919 		 * Close the theme details modal.
       
   920 		 *
       
   921 		 * @since 4.2.0
       
   922 		 */
       
   923 		closeDetails: function () {
       
   924 			$( 'body' ).removeClass( 'modal-open' );
       
   925 			this.overlay.fadeOut( 'fast' );
       
   926 			api.control( 'theme_' + this.currentTheme ).focus();
       
   927 		},
       
   928 
       
   929 		/**
       
   930 		 * Keep tab focus within the theme details modal.
       
   931 		 *
       
   932 		 * @since 4.2.0
       
   933 		 */
       
   934 		containFocus: function( el ) {
       
   935 			var tabbables;
       
   936 
       
   937 			el.on( 'keydown', function( event ) {
       
   938 
       
   939 				// Return if it's not the tab key
       
   940 				// When navigating with prev/next focus is already handled
       
   941 				if ( 9 !== event.keyCode ) {
       
   942 					return;
       
   943 				}
       
   944 
       
   945 				// uses jQuery UI to get the tabbable elements
       
   946 				tabbables = $( ':tabbable', el );
       
   947 
       
   948 				// Keep focus within the overlay
       
   949 				if ( tabbables.last()[0] === event.target && ! event.shiftKey ) {
       
   950 					tabbables.first().focus();
       
   951 					return false;
       
   952 				} else if ( tabbables.first()[0] === event.target && event.shiftKey ) {
       
   953 					tabbables.last().focus();
       
   954 					return false;
       
   955 				}
       
   956 			});
       
   957 		}
       
   958 	});
       
   959 
       
   960 	/**
       
   961 	 * @since 4.1.0
       
   962 	 *
       
   963 	 * @class
       
   964 	 * @augments wp.customize.Class
       
   965 	 */
       
   966 	api.Panel = Container.extend({
       
   967 		/**
       
   968 		 * @since 4.1.0
       
   969 		 *
       
   970 		 * @param  {String} id
       
   971 		 * @param  {Object} options
       
   972 		 */
       
   973 		initialize: function ( id, options ) {
       
   974 			var panel = this;
       
   975 			Container.prototype.initialize.call( panel, id, options );
       
   976 			panel.embed();
       
   977 			panel.deferred.embedded.done( function () {
       
   978 				panel.ready();
       
   979 			});
       
   980 		},
       
   981 
       
   982 		/**
       
   983 		 * Embed the container in the DOM when any parent panel is ready.
       
   984 		 *
       
   985 		 * @since 4.1.0
       
   986 		 */
       
   987 		embed: function () {
       
   988 			var panel = this,
       
   989 				parentContainer = $( '#customize-theme-controls > ul' ); // @todo This should be defined elsewhere, and to be configurable
       
   990 
       
   991 			if ( ! panel.container.parent().is( parentContainer ) ) {
       
   992 				parentContainer.append( panel.container );
       
   993 			}
       
   994 			panel.deferred.embedded.resolve();
       
   995 		},
       
   996 
       
   997 		/**
       
   998 		 * @since 4.1.0
       
   999 		 */
       
  1000 		attachEvents: function () {
       
  1001 			var meta, panel = this;
       
  1002 
       
  1003 			// Expand/Collapse accordion sections on click.
       
  1004 			panel.container.find( '.accordion-section-title' ).on( 'click keydown', function( event ) {
       
  1005 				if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
       
  1006 					return;
       
  1007 				}
       
  1008 				event.preventDefault(); // Keep this AFTER the key filter above
       
  1009 
       
  1010 				if ( ! panel.expanded() ) {
       
  1011 					panel.expand();
       
  1012 				}
       
  1013 			});
       
  1014 
       
  1015 			meta = panel.container.find( '.panel-meta:first' );
       
  1016 
       
  1017 			meta.find( '> .accordion-section-title' ).on( 'click keydown', function( event ) {
       
  1018 				if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
       
  1019 					return;
       
  1020 				}
       
  1021 				event.preventDefault(); // Keep this AFTER the key filter above
       
  1022 
       
  1023 				if ( meta.hasClass( 'cannot-expand' ) ) {
       
  1024 					return;
       
  1025 				}
       
  1026 
       
  1027 				var content = meta.find( '.accordion-section-content:first' );
       
  1028 				if ( meta.hasClass( 'open' ) ) {
       
  1029 					meta.toggleClass( 'open' );
       
  1030 					content.slideUp( panel.defaultExpandedArguments.duration );
       
  1031 				} else {
       
  1032 					content.slideDown( panel.defaultExpandedArguments.duration );
       
  1033 					meta.toggleClass( 'open' );
       
  1034 				}
       
  1035 			});
       
  1036 
       
  1037 		},
       
  1038 
       
  1039 		/**
       
  1040 		 * Get the sections that are associated with this panel, sorted by their priority Value.
       
  1041 		 *
       
  1042 		 * @since 4.1.0
       
  1043 		 *
       
  1044 		 * @returns {Array}
       
  1045 		 */
       
  1046 		sections: function () {
       
  1047 			return this._children( 'panel', 'section' );
       
  1048 		},
       
  1049 
       
  1050 		/**
       
  1051 		 * Return whether this panel has any active sections.
       
  1052 		 *
       
  1053 		 * @since 4.1.0
       
  1054 		 *
       
  1055 		 * @returns {boolean}
       
  1056 		 */
       
  1057 		isContextuallyActive: function () {
       
  1058 			var panel = this,
       
  1059 				sections = panel.sections(),
       
  1060 				activeCount = 0;
       
  1061 			_( sections ).each( function ( section ) {
       
  1062 				if ( section.active() && section.isContextuallyActive() ) {
       
  1063 					activeCount += 1;
       
  1064 				}
       
  1065 			} );
       
  1066 			return ( activeCount !== 0 );
       
  1067 		},
       
  1068 
       
  1069 		/**
       
  1070 		 * Update UI to reflect expanded state
       
  1071 		 *
       
  1072 		 * @since 4.1.0
       
  1073 		 *
       
  1074 		 * @param {Boolean}  expanded
       
  1075 		 * @param {Object}   args
       
  1076 		 * @param {Boolean}  args.unchanged
       
  1077 		 * @param {Callback} args.completeCallback
       
  1078 		 */
       
  1079 		onChangeExpanded: function ( expanded, args ) {
       
  1080 
       
  1081 			// Immediately call the complete callback if there were no changes
       
  1082 			if ( args.unchanged ) {
       
  1083 				if ( args.completeCallback ) {
       
  1084 					args.completeCallback();
       
  1085 				}
       
  1086 				return;
       
  1087 			}
       
  1088 
       
  1089 			// Note: there is a second argument 'args' passed
       
  1090 			var position, scroll,
       
  1091 				panel = this,
       
  1092 				section = panel.container.closest( '.accordion-section' ),
       
  1093 				overlay = section.closest( '.wp-full-overlay' ),
       
  1094 				container = section.closest( '.wp-full-overlay-sidebar-content' ),
       
  1095 				siblings = container.find( '.open' ),
       
  1096 				topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ).add( '#customize-info > .accordion-section-title' ),
       
  1097 				backBtn = overlay.find( '.control-panel-back' ),
       
  1098 				panelTitle = section.find( '.accordion-section-title' ).first(),
       
  1099 				content = section.find( '.control-panel-content' );
       
  1100 
       
  1101 			if ( expanded ) {
       
  1102 
       
  1103 				// Collapse any sibling sections/panels
       
  1104 				api.section.each( function ( section ) {
       
  1105 					if ( ! section.panel() ) {
       
  1106 						section.collapse( { duration: 0 } );
       
  1107 					}
       
  1108 				});
       
  1109 				api.panel.each( function ( otherPanel ) {
       
  1110 					if ( panel !== otherPanel ) {
       
  1111 						otherPanel.collapse( { duration: 0 } );
       
  1112 					}
       
  1113 				});
       
  1114 
       
  1115 				content.show( 0, function() {
       
  1116 					content.parent().show();
       
  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' );
       
  1122 					container.scrollTop( 0 );
       
  1123 					if ( args.completeCallback ) {
       
  1124 						args.completeCallback();
       
  1125 					}
       
  1126 				} );
       
  1127 				topPanel.attr( 'tabindex', '-1' );
       
  1128 				backBtn.attr( 'tabindex', '0' );
       
  1129 				backBtn.focus();
       
  1130 			} else {
       
  1131 				siblings.removeClass( 'open' );
       
  1132 				section.removeClass( 'current-panel' );
       
  1133 				overlay.removeClass( 'in-sub-panel' );
       
  1134 				content.delay( 180 ).hide( 0, function() {
       
  1135 					content.css( 'margin-top', 'inherit' ); // Reset
       
  1136 					if ( args.completeCallback ) {
       
  1137 						args.completeCallback();
       
  1138 					}
       
  1139 				} );
       
  1140 				topPanel.attr( 'tabindex', '0' );
       
  1141 				backBtn.attr( 'tabindex', '-1' );
       
  1142 				panelTitle.focus();
       
  1143 				container.scrollTop( 0 );
       
  1144 			}
       
  1145 		}
       
  1146 	});
       
  1147 
       
  1148 	/**
       
  1149 	 * A Customizer Control.
       
  1150 	 *
       
  1151 	 * A control provides a UI element that allows a user to modify a Customizer Setting.
       
  1152 	 *
       
  1153 	 * @see PHP class WP_Customize_Control.
       
  1154 	 *
       
  1155 	 * @class
       
  1156 	 * @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 	 */
    30 	api.Control = api.Class.extend({
  1170 	api.Control = api.Class.extend({
       
  1171 		defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
       
  1172 
    31 		initialize: function( id, options ) {
  1173 		initialize: function( id, options ) {
    32 			var control = this,
  1174 			var control = this,
    33 				nodes, radios, settings;
  1175 				nodes, radios, settings;
    34 
  1176 
    35 			this.params = {};
  1177 			control.params = {};
    36 			$.extend( this, options || {} );
  1178 			$.extend( control, options || {} );
    37 
  1179 			control.id = id;
    38 			this.id = id;
  1180 			control.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' );
    39 			this.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' );
  1181 			control.templateSelector = 'customize-control-' + control.params.type + '-content';
    40 			this.container = $( this.selector );
  1182 			control.container = control.params.content ? $( control.params.content ) : $( control.selector );
    41 
  1183 
    42 			settings = $.map( this.params.settings, function( value ) {
  1184 			control.deferred = {
    43 				return value;
  1185 				embedded: new $.Deferred()
    44 			});
  1186 			};
    45 
  1187 			control.section = new api.Value();
    46 			api.apply( api, settings.concat( function() {
  1188 			control.priority = new api.Value();
    47 				var key;
  1189 			control.active = new api.Value();
    48 
  1190 			control.activeArgumentsQueue = [];
    49 				control.settings = {};
       
    50 				for ( key in control.params.settings ) {
       
    51 					control.settings[ key ] = api( control.params.settings[ key ] );
       
    52 				}
       
    53 
       
    54 				control.setting = control.settings['default'] || null;
       
    55 				control.ready();
       
    56 			}) );
       
    57 
  1191 
    58 			control.elements = [];
  1192 			control.elements = [];
    59 
  1193 
    60 			nodes  = this.container.find('[data-customize-setting-link]');
  1194 			nodes  = control.container.find('[data-customize-setting-link]');
    61 			radios = {};
  1195 			radios = {};
    62 
  1196 
    63 			nodes.each( function() {
  1197 			nodes.each( function() {
    64 				var node = $(this),
  1198 				var node = $( this ),
    65 					name;
  1199 					name;
    66 
  1200 
    67 				if ( node.is(':radio') ) {
  1201 				if ( node.is( ':radio' ) ) {
    68 					name = node.prop('name');
  1202 					name = node.prop( 'name' );
    69 					if ( radios[ name ] )
  1203 					if ( radios[ name ] ) {
    70 						return;
  1204 						return;
       
  1205 					}
    71 
  1206 
    72 					radios[ name ] = true;
  1207 					radios[ name ] = true;
    73 					node = nodes.filter( '[name="' + name + '"]' );
  1208 					node = nodes.filter( '[name="' + name + '"]' );
    74 				}
  1209 				}
    75 
  1210 
    76 				api( node.data('customizeSettingLink'), function( setting ) {
  1211 				api( node.data( 'customizeSettingLink' ), function( setting ) {
    77 					var element = new api.Element( node );
  1212 					var element = new api.Element( node );
    78 					control.elements.push( element );
  1213 					control.elements.push( element );
    79 					element.sync( setting );
  1214 					element.sync( setting );
    80 					element.set( setting() );
  1215 					element.set( setting() );
    81 				});
  1216 				});
    82 			});
  1217 			});
    83 		},
  1218 
    84 
  1219 			control.active.bind( function ( active ) {
       
  1220 				var args = control.activeArgumentsQueue.shift();
       
  1221 				args = $.extend( {}, control.defaultActiveArguments, args );
       
  1222 				control.onChangeActive( active, args );
       
  1223 			} );
       
  1224 
       
  1225 			control.section.set( control.params.section );
       
  1226 			control.priority.set( isNaN( control.params.priority ) ? 10 : control.params.priority );
       
  1227 			control.active.set( control.params.active );
       
  1228 
       
  1229 			api.utils.bubbleChildValueChanges( control, [ 'section', 'priority', 'active' ] );
       
  1230 
       
  1231 			// Associate this control with its settings when they are created
       
  1232 			settings = $.map( control.params.settings, function( value ) {
       
  1233 				return value;
       
  1234 			});
       
  1235 			api.apply( api, settings.concat( function () {
       
  1236 				var key;
       
  1237 
       
  1238 				control.settings = {};
       
  1239 				for ( key in control.params.settings ) {
       
  1240 					control.settings[ key ] = api( control.params.settings[ key ] );
       
  1241 				}
       
  1242 
       
  1243 				control.setting = control.settings['default'] || null;
       
  1244 
       
  1245 				control.embed();
       
  1246 			}) );
       
  1247 
       
  1248 			control.deferred.embedded.done( function () {
       
  1249 				control.ready();
       
  1250 			});
       
  1251 		},
       
  1252 
       
  1253 		/**
       
  1254 		 * Embed the control into the page.
       
  1255 		 */
       
  1256 		embed: function () {
       
  1257 			var control = this,
       
  1258 				inject;
       
  1259 
       
  1260 			// Watch for changes to the section state
       
  1261 			inject = function ( sectionId ) {
       
  1262 				var parentContainer;
       
  1263 				if ( ! sectionId ) { // @todo allow a control to be embedded without a section, for instance a control embedded in the frontend
       
  1264 					return;
       
  1265 				}
       
  1266 				// Wait for the section to be registered
       
  1267 				api.section( sectionId, function ( section ) {
       
  1268 					// Wait for the section to be ready/initialized
       
  1269 					section.deferred.embedded.done( function () {
       
  1270 						parentContainer = section.container.find( 'ul:first' );
       
  1271 						if ( ! control.container.parent().is( parentContainer ) ) {
       
  1272 							parentContainer.append( control.container );
       
  1273 							control.renderContent();
       
  1274 						}
       
  1275 						control.deferred.embedded.resolve();
       
  1276 					});
       
  1277 				});
       
  1278 			};
       
  1279 			control.section.bind( inject );
       
  1280 			inject( control.section.get() );
       
  1281 		},
       
  1282 
       
  1283 		/**
       
  1284 		 * Triggered when the control's markup has been injected into the DOM.
       
  1285 		 *
       
  1286 		 * @abstract
       
  1287 		 */
    85 		ready: function() {},
  1288 		ready: function() {},
    86 
  1289 
       
  1290 		/**
       
  1291 		 * Normal controls do not expand, so just expand its parent
       
  1292 		 *
       
  1293 		 * @param {Object} [params]
       
  1294 		 */
       
  1295 		expand: function ( params ) {
       
  1296 			api.section( this.section() ).expand( params );
       
  1297 		},
       
  1298 
       
  1299 		/**
       
  1300 		 * Bring the containing section and panel into view and then
       
  1301 		 * this control into view, focusing on the first input.
       
  1302 		 */
       
  1303 		focus: focus,
       
  1304 
       
  1305 		/**
       
  1306 		 * Update UI in response to a change in the control's active state.
       
  1307 		 * This does not change the active state, it merely handles the behavior
       
  1308 		 * for when it does change.
       
  1309 		 *
       
  1310 		 * @since 4.1.0
       
  1311 		 *
       
  1312 		 * @param {Boolean}  active
       
  1313 		 * @param {Object}   args
       
  1314 		 * @param {Number}   args.duration
       
  1315 		 * @param {Callback} args.completeCallback
       
  1316 		 */
       
  1317 		onChangeActive: function ( active, args ) {
       
  1318 			if ( ! $.contains( document, this.container ) ) {
       
  1319 				// jQuery.fn.slideUp is not hiding an element if it is not in the DOM
       
  1320 				this.container.toggle( active );
       
  1321 				if ( args.completeCallback ) {
       
  1322 					args.completeCallback();
       
  1323 				}
       
  1324 			} else if ( active ) {
       
  1325 				this.container.slideDown( args.duration, args.completeCallback );
       
  1326 			} else {
       
  1327 				this.container.slideUp( args.duration, args.completeCallback );
       
  1328 			}
       
  1329 		},
       
  1330 
       
  1331 		/**
       
  1332 		 * @deprecated 4.1.0 Use this.onChangeActive() instead.
       
  1333 		 */
       
  1334 		toggle: function ( active ) {
       
  1335 			return this.onChangeActive( active, this.defaultActiveArguments );
       
  1336 		},
       
  1337 
       
  1338 		/**
       
  1339 		 * Shorthand way to enable the active state.
       
  1340 		 *
       
  1341 		 * @since 4.1.0
       
  1342 		 *
       
  1343 		 * @param {Object} [params]
       
  1344 		 * @returns {Boolean} false if already active
       
  1345 		 */
       
  1346 		activate: Container.prototype.activate,
       
  1347 
       
  1348 		/**
       
  1349 		 * Shorthand way to disable the active state.
       
  1350 		 *
       
  1351 		 * @since 4.1.0
       
  1352 		 *
       
  1353 		 * @param {Object} [params]
       
  1354 		 * @returns {Boolean} false if already inactive
       
  1355 		 */
       
  1356 		deactivate: Container.prototype.deactivate,
       
  1357 
       
  1358 		/**
       
  1359 		 * Re-use _toggleActive from Container class.
       
  1360 		 *
       
  1361 		 * @access private
       
  1362 		 */
       
  1363 		_toggleActive: Container.prototype._toggleActive,
       
  1364 
    87 		dropdownInit: function() {
  1365 		dropdownInit: function() {
    88 			var control  = this,
  1366 			var control      = this,
    89 				statuses = this.container.find('.dropdown-status'),
  1367 				statuses     = this.container.find('.dropdown-status'),
    90 				params   = this.params,
  1368 				params       = this.params,
    91 				update   = function( to ) {
  1369 				toggleFreeze = false,
    92 					if ( typeof	to === 'string' && params.statuses && params.statuses[ to ] )
  1370 				update       = function( to ) {
       
  1371 					if ( typeof to === 'string' && params.statuses && params.statuses[ to ] )
    93 						statuses.html( params.statuses[ to ] ).show();
  1372 						statuses.html( params.statuses[ to ] ).show();
    94 					else
  1373 					else
    95 						statuses.hide();
  1374 						statuses.hide();
    96 				};
  1375 				};
    97 
  1376 
    98 			var toggleFreeze = false;
       
    99 
       
   100 			// Support the .dropdown class to open/close complex elements
  1377 			// Support the .dropdown class to open/close complex elements
   101 			this.container.on( 'click keydown', '.dropdown', function( event ) {
  1378 			this.container.on( 'click keydown', '.dropdown', function( event ) {
   102 				if ( event.type === 'keydown' &&  13 !== event.which ) // enter
  1379 				if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
   103 					return;
  1380 					return;
       
  1381 				}
   104 
  1382 
   105 				event.preventDefault();
  1383 				event.preventDefault();
   106 
  1384 
   107 				if (!toggleFreeze)
  1385 				if (!toggleFreeze)
   108 					control.container.toggleClass('open');
  1386 					control.container.toggleClass('open');
   117 				}, 400);
  1395 				}, 400);
   118 			});
  1396 			});
   119 
  1397 
   120 			this.setting.bind( update );
  1398 			this.setting.bind( update );
   121 			update( this.setting() );
  1399 			update( this.setting() );
       
  1400 		},
       
  1401 
       
  1402 		/**
       
  1403 		 * Render the control from its JS template, if it exists.
       
  1404 		 *
       
  1405 		 * The control's container must already exist in the DOM.
       
  1406 		 *
       
  1407 		 * @since 4.1.0
       
  1408 		 */
       
  1409 		renderContent: function () {
       
  1410 			var template,
       
  1411 				control = this;
       
  1412 
       
  1413 			// Replace the container element's content with the control.
       
  1414 			if ( 0 !== $( '#tmpl-' + control.templateSelector ).length ) {
       
  1415 				template = wp.template( control.templateSelector );
       
  1416 				if ( template && control.container ) {
       
  1417 					control.container.html( template( control.params ) );
       
  1418 				}
       
  1419 			}
   122 		}
  1420 		}
   123 	});
  1421 	});
   124 
  1422 
       
  1423 	/**
       
  1424 	 * A colorpicker control.
       
  1425 	 *
       
  1426 	 * @class
       
  1427 	 * @augments wp.customize.Control
       
  1428 	 * @augments wp.customize.Class
       
  1429 	 */
   125 	api.ColorControl = api.Control.extend({
  1430 	api.ColorControl = api.Control.extend({
   126 		ready: function() {
  1431 		ready: function() {
   127 			var control = this,
  1432 			var control = this,
   128 				picker = this.container.find('.color-picker-hex');
  1433 				picker = this.container.find('.color-picker-hex');
   129 
  1434 
   130 			picker.val( control.setting() ).wpColorPicker({
  1435 			picker.val( control.setting() ).wpColorPicker({
   131 				change: function( event, options ) {
  1436 				change: function() {
   132 					control.setting.set( picker.wpColorPicker('color') );
  1437 					control.setting.set( picker.wpColorPicker('color') );
   133  				},
  1438 				},
   134  				clear: function() {
  1439 				clear: function() {
   135  					control.setting.set( false );
  1440 					control.setting.set( false );
   136  				}
  1441 				}
       
  1442 			});
       
  1443 
       
  1444 			this.setting.bind( function ( value ) {
       
  1445 				picker.val( value );
       
  1446 				picker.wpColorPicker( 'color', value );
   137 			});
  1447 			});
   138 		}
  1448 		}
   139 	});
  1449 	});
   140 
  1450 
   141 	api.UploadControl = api.Control.extend({
  1451 	/**
       
  1452 	 * A control that implements the media modal.
       
  1453 	 *
       
  1454 	 * @class
       
  1455 	 * @augments wp.customize.Control
       
  1456 	 * @augments wp.customize.Class
       
  1457 	 */
       
  1458 	api.MediaControl = api.Control.extend({
       
  1459 
       
  1460 		/**
       
  1461 		 * When the control's DOM structure is ready,
       
  1462 		 * set up internal event bindings.
       
  1463 		 */
   142 		ready: function() {
  1464 		ready: function() {
   143 			var control = this;
  1465 			var control = this;
   144 
  1466 			// Shortcut so that we don't have to use _.bind every time we add a callback.
   145 			this.params.removed = this.params.removed || '';
  1467 			_.bindAll( control, 'restoreDefault', 'removeFile', 'openFrame', 'select', 'pausePlayer' );
   146 
  1468 
   147 			this.success = $.proxy( this.success, this );
  1469 			// Bind events, with delegation to facilitate re-rendering.
   148 
  1470 			control.container.on( 'click keydown', '.upload-button', control.openFrame );
   149 			this.uploader = $.extend({
  1471 			control.container.on( 'click keydown', '.upload-button', control.pausePlayer );
   150 				container: this.container,
  1472 			control.container.on( 'click keydown', '.thumbnail-image img', control.openFrame );
   151 				browser:   this.container.find('.upload'),
  1473 			control.container.on( 'click keydown', '.default-button', control.restoreDefault );
   152 				dropzone:  this.container.find('.upload-dropzone'),
  1474 			control.container.on( 'click keydown', '.remove-button', control.pausePlayer );
   153 				success:   this.success,
  1475 			control.container.on( 'click keydown', '.remove-button', control.removeFile );
   154 				plupload:  {},
  1476 			control.container.on( 'click keydown', '.remove-button', control.cleanupPlayer );
   155 				params:    {}
  1477 
   156 			}, this.uploader || {} );
  1478 			// Resize the player controls when it becomes visible (ie when section is expanded)
   157 
  1479 			api.section( control.section() ).container
   158 			if ( control.params.extensions ) {
  1480 				.on( 'expanded', function() {
   159 				control.uploader.plupload.filters = [{
  1481 					if ( control.player ) {
   160 					title:      api.l10n.allowedFiles,
  1482 						control.player.setControlsSize();
   161 					extensions: control.params.extensions
  1483 					}
   162 				}];
  1484 				})
   163 			}
  1485 				.on( 'collapsed', function() {
   164 
  1486 					control.pausePlayer();
   165 			if ( control.params.context )
  1487 				});
   166 				control.uploader.params['post_data[context]'] = this.params.context;
  1488 
   167 
  1489 			// Re-render whenever the control's setting changes.
   168 			if ( api.settings.theme.stylesheet )
  1490 			control.setting.bind( function () { control.renderContent(); } );
   169 				control.uploader.params['post_data[theme]'] = api.settings.theme.stylesheet;
  1491 		},
   170 
  1492 
   171 			this.uploader = new wp.Uploader( this.uploader );
  1493 		pausePlayer: function () {
   172 
  1494 			this.player && this.player.pause();
   173 			this.remover = this.container.find('.remove');
  1495 		},
   174 			this.remover.on( 'click keydown', function( event ) {
  1496 
   175 				if ( event.type === 'keydown' &&  13 !== event.which ) // enter
  1497 		cleanupPlayer: function () {
   176 					return;
  1498 			this.player && wp.media.mixin.removePlayer( this.player );
   177 
  1499 		},
   178 				control.setting.set( control.params.removed );
  1500 
   179 				event.preventDefault();
  1501 		/**
   180 			});
  1502 		 * Open the media modal.
   181 
  1503 		 */
   182 			this.removerVisibility = $.proxy( this.removerVisibility, this );
  1504 		openFrame: function( event ) {
   183 			this.setting.bind( this.removerVisibility );
  1505 			if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
   184 			this.removerVisibility( this.setting.get() );
  1506 				return;
   185 		},
  1507 			}
   186 		success: function( attachment ) {
  1508 
   187 			this.setting.set( attachment.get('url') );
  1509 			event.preventDefault();
   188 		},
  1510 
   189 		removerVisibility: function( to ) {
  1511 			if ( ! this.frame ) {
   190 			this.remover.toggle( to != this.params.removed );
  1512 				this.initFrame();
       
  1513 			}
       
  1514 
       
  1515 			this.frame.open();
       
  1516 		},
       
  1517 
       
  1518 		/**
       
  1519 		 * Create a media modal select frame, and store it so the instance can be reused when needed.
       
  1520 		 */
       
  1521 		initFrame: function() {
       
  1522 			this.frame = wp.media({
       
  1523 				button: {
       
  1524 					text: this.params.button_labels.frame_button
       
  1525 				},
       
  1526 				states: [
       
  1527 					new wp.media.controller.Library({
       
  1528 						title:     this.params.button_labels.frame_title,
       
  1529 						library:   wp.media.query({ type: this.params.mime_type }),
       
  1530 						multiple:  false,
       
  1531 						date:      false
       
  1532 					})
       
  1533 				]
       
  1534 			});
       
  1535 
       
  1536 			// When a file is selected, run a callback.
       
  1537 			this.frame.on( 'select', this.select );
       
  1538 		},
       
  1539 
       
  1540 		/**
       
  1541 		 * Callback handler for when an attachment is selected in the media modal.
       
  1542 		 * Gets the selected image information, and sets it within the control.
       
  1543 		 */
       
  1544 		select: function() {
       
  1545 			// Get the attachment from the modal frame.
       
  1546 			var node,
       
  1547 				attachment = this.frame.state().get( 'selection' ).first().toJSON(),
       
  1548 				mejsSettings = window._wpmejsSettings || {};
       
  1549 
       
  1550 			this.params.attachment = attachment;
       
  1551 
       
  1552 			// Set the Customizer setting; the callback takes care of rendering.
       
  1553 			this.setting( attachment.id );
       
  1554 			node = this.container.find( 'audio, video' ).get(0);
       
  1555 
       
  1556 			// Initialize audio/video previews.
       
  1557 			if ( node ) {
       
  1558 				this.player = new MediaElementPlayer( node, mejsSettings );
       
  1559 			} else {
       
  1560 				this.cleanupPlayer();
       
  1561 			}
       
  1562 		},
       
  1563 
       
  1564 		/**
       
  1565 		 * Reset the setting to the default value.
       
  1566 		 */
       
  1567 		restoreDefault: function( event ) {
       
  1568 			if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
       
  1569 				return;
       
  1570 			}
       
  1571 			event.preventDefault();
       
  1572 
       
  1573 			this.params.attachment = this.params.defaultAttachment;
       
  1574 			this.setting( this.params.defaultAttachment.url );
       
  1575 		},
       
  1576 
       
  1577 		/**
       
  1578 		 * Called when the "Remove" link is clicked. Empties the setting.
       
  1579 		 *
       
  1580 		 * @param {object} event jQuery Event object
       
  1581 		 */
       
  1582 		removeFile: function( event ) {
       
  1583 			if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
       
  1584 				return;
       
  1585 			}
       
  1586 			event.preventDefault();
       
  1587 
       
  1588 			this.params.attachment = {};
       
  1589 			this.setting( '' );
       
  1590 			this.renderContent(); // Not bound to setting change when emptying.
   191 		}
  1591 		}
   192 	});
  1592 	});
   193 
  1593 
       
  1594 	/**
       
  1595 	 * An upload control, which utilizes the media modal.
       
  1596 	 *
       
  1597 	 * @class
       
  1598 	 * @augments wp.customize.MediaControl
       
  1599 	 * @augments wp.customize.Control
       
  1600 	 * @augments wp.customize.Class
       
  1601 	 */
       
  1602 	api.UploadControl = api.MediaControl.extend({
       
  1603 
       
  1604 		/**
       
  1605 		 * Callback handler for when an attachment is selected in the media modal.
       
  1606 		 * Gets the selected image information, and sets it within the control.
       
  1607 		 */
       
  1608 		select: function() {
       
  1609 			// Get the attachment from the modal frame.
       
  1610 			var node,
       
  1611 				attachment = this.frame.state().get( 'selection' ).first().toJSON(),
       
  1612 				mejsSettings = window._wpmejsSettings || {};
       
  1613 
       
  1614 			this.params.attachment = attachment;
       
  1615 
       
  1616 			// Set the Customizer setting; the callback takes care of rendering.
       
  1617 			this.setting( attachment.url );
       
  1618 			node = this.container.find( 'audio, video' ).get(0);
       
  1619 
       
  1620 			// Initialize audio/video previews.
       
  1621 			if ( node ) {
       
  1622 				this.player = new MediaElementPlayer( node, mejsSettings );
       
  1623 			} else {
       
  1624 				this.cleanupPlayer();
       
  1625 			}
       
  1626 		},
       
  1627 
       
  1628 		// @deprecated
       
  1629 		success: function() {},
       
  1630 
       
  1631 		// @deprecated
       
  1632 		removerVisibility: function() {}
       
  1633 	});
       
  1634 
       
  1635 	/**
       
  1636 	 * A control for uploading images.
       
  1637 	 *
       
  1638 	 * This control no longer needs to do anything more
       
  1639 	 * than what the upload control does in JS.
       
  1640 	 *
       
  1641 	 * @class
       
  1642 	 * @augments wp.customize.UploadControl
       
  1643 	 * @augments wp.customize.MediaControl
       
  1644 	 * @augments wp.customize.Control
       
  1645 	 * @augments wp.customize.Class
       
  1646 	 */
   194 	api.ImageControl = api.UploadControl.extend({
  1647 	api.ImageControl = api.UploadControl.extend({
       
  1648 		// @deprecated
       
  1649 		thumbnailSrc: function() {}
       
  1650 	});
       
  1651 
       
  1652 	/**
       
  1653 	 * A control for uploading background images.
       
  1654 	 *
       
  1655 	 * @class
       
  1656 	 * @augments wp.customize.UploadControl
       
  1657 	 * @augments wp.customize.MediaControl
       
  1658 	 * @augments wp.customize.Control
       
  1659 	 * @augments wp.customize.Class
       
  1660 	 */
       
  1661 	api.BackgroundControl = api.UploadControl.extend({
       
  1662 
       
  1663 		/**
       
  1664 		 * When the control's DOM structure is ready,
       
  1665 		 * set up internal event bindings.
       
  1666 		 */
   195 		ready: function() {
  1667 		ready: function() {
   196 			var control = this,
  1668 			api.UploadControl.prototype.ready.apply( this, arguments );
   197 				panels;
  1669 		},
   198 
  1670 
   199 			this.uploader = {
  1671 		/**
   200 				init: function( up ) {
  1672 		 * Callback handler for when an attachment is selected in the media modal.
   201 					var fallback, button;
  1673 		 * Does an additional AJAX request for setting the background context.
   202 
  1674 		 */
   203 					if ( this.supports.dragdrop )
  1675 		select: function() {
   204 						return;
  1676 			api.UploadControl.prototype.select.apply( this, arguments );
   205 
  1677 
   206 					// Maintain references while wrapping the fallback button.
  1678 			wp.ajax.post( 'custom-background-add', {
   207 					fallback = control.container.find( '.upload-fallback' );
  1679 				nonce: _wpCustomizeBackground.nonces.add,
   208 					button   = fallback.children().detach();
  1680 				wp_customize: 'on',
   209 
  1681 				theme: api.settings.theme.stylesheet,
   210 					this.browser.detach().empty().append( button );
  1682 				attachment_id: this.params.attachment.id
   211 					fallback.append( this.browser ).show();
  1683 			} );
   212 				}
       
   213 			};
       
   214 
       
   215 			api.UploadControl.prototype.ready.call( this );
       
   216 
       
   217 			this.thumbnail    = this.container.find('.preview-thumbnail img');
       
   218 			this.thumbnailSrc = $.proxy( this.thumbnailSrc, this );
       
   219 			this.setting.bind( this.thumbnailSrc );
       
   220 
       
   221 			this.library = this.container.find('.library');
       
   222 
       
   223 			// Generate tab objects
       
   224 			this.tabs = {};
       
   225 			panels    = this.library.find('.library-content');
       
   226 
       
   227 			this.library.children('ul').children('li').each( function() {
       
   228 				var link  = $(this),
       
   229 					id    = link.data('customizeTab'),
       
   230 					panel = panels.filter('[data-customize-tab="' + id + '"]');
       
   231 
       
   232 				control.tabs[ id ] = {
       
   233 					both:  link.add( panel ),
       
   234 					link:  link,
       
   235 					panel: panel
       
   236 				};
       
   237 			});
       
   238 
       
   239 			// Bind tab switch events
       
   240 			this.library.children('ul').on( 'click keydown', 'li', function( event ) {
       
   241 				if ( event.type === 'keydown' &&  13 !== event.which ) // enter
       
   242 					return;
       
   243 
       
   244 				var id  = $(this).data('customizeTab'),
       
   245 					tab = control.tabs[ id ];
       
   246 
       
   247 				event.preventDefault();
       
   248 
       
   249 				if ( tab.link.hasClass('library-selected') )
       
   250 					return;
       
   251 
       
   252 				control.selected.both.removeClass('library-selected');
       
   253 				control.selected = tab;
       
   254 				control.selected.both.addClass('library-selected');
       
   255 			});
       
   256 
       
   257 			// Bind events to switch image urls.
       
   258 			this.library.on( 'click keydown', 'a', function( event ) {
       
   259 				if ( event.type === 'keydown' && 13 !== event.which ) // enter
       
   260 					return;
       
   261 
       
   262 				var value = $(this).data('customizeImageValue');
       
   263 
       
   264 				if ( value ) {
       
   265 					control.setting.set( value );
       
   266 					event.preventDefault();
       
   267 				}
       
   268 			});
       
   269 
       
   270 			if ( this.tabs.uploaded ) {
       
   271 				this.tabs.uploaded.target = this.library.find('.uploaded-target');
       
   272 				if ( ! this.tabs.uploaded.panel.find('.thumbnail').length )
       
   273 					this.tabs.uploaded.both.addClass('hidden');
       
   274 			}
       
   275 
       
   276 			// Select a tab
       
   277 			panels.each( function() {
       
   278 				var tab = control.tabs[ $(this).data('customizeTab') ];
       
   279 
       
   280 				// Select the first visible tab.
       
   281 				if ( ! tab.link.hasClass('hidden') ) {
       
   282 					control.selected = tab;
       
   283 					tab.both.addClass('library-selected');
       
   284 					return false;
       
   285 				}
       
   286 			});
       
   287 
       
   288 			this.dropdownInit();
       
   289 		},
       
   290 		success: function( attachment ) {
       
   291 			api.UploadControl.prototype.success.call( this, attachment );
       
   292 
       
   293 			// Add the uploaded image to the uploaded tab.
       
   294 			if ( this.tabs.uploaded && this.tabs.uploaded.target.length ) {
       
   295 				this.tabs.uploaded.both.removeClass('hidden');
       
   296 
       
   297 				// @todo: Do NOT store this on the attachment model. That is bad.
       
   298 				attachment.element = $( '<a href="#" class="thumbnail"></a>' )
       
   299 					.data( 'customizeImageValue', attachment.get('url') )
       
   300 					.append( '<img src="' +  attachment.get('url')+ '" />' )
       
   301 					.appendTo( this.tabs.uploaded.target );
       
   302 			}
       
   303 		},
       
   304 		thumbnailSrc: function( to ) {
       
   305 			if ( /^(https?:)?\/\//.test( to ) )
       
   306 				this.thumbnail.prop( 'src', to ).show();
       
   307 			else
       
   308 				this.thumbnail.hide();
       
   309 		}
  1684 		}
   310 	});
  1685 	});
   311 
  1686 
       
  1687 	/**
       
  1688 	 * @class
       
  1689 	 * @augments wp.customize.Control
       
  1690 	 * @augments wp.customize.Class
       
  1691 	 */
       
  1692 	api.HeaderControl = api.Control.extend({
       
  1693 		ready: function() {
       
  1694 			this.btnRemove = $('#customize-control-header_image .actions .remove');
       
  1695 			this.btnNew    = $('#customize-control-header_image .actions .new');
       
  1696 
       
  1697 			_.bindAll(this, 'openMedia', 'removeImage');
       
  1698 
       
  1699 			this.btnNew.on( 'click', this.openMedia );
       
  1700 			this.btnRemove.on( 'click', this.removeImage );
       
  1701 
       
  1702 			api.HeaderTool.currentHeader = this.getInitialHeaderImage();
       
  1703 
       
  1704 			new api.HeaderTool.CurrentView({
       
  1705 				model: api.HeaderTool.currentHeader,
       
  1706 				el: '#customize-control-header_image .current .container'
       
  1707 			});
       
  1708 
       
  1709 			new api.HeaderTool.ChoiceListView({
       
  1710 				collection: api.HeaderTool.UploadsList = new api.HeaderTool.ChoiceList(),
       
  1711 				el: '#customize-control-header_image .choices .uploaded .list'
       
  1712 			});
       
  1713 
       
  1714 			new api.HeaderTool.ChoiceListView({
       
  1715 				collection: api.HeaderTool.DefaultsList = new api.HeaderTool.DefaultsList(),
       
  1716 				el: '#customize-control-header_image .choices .default .list'
       
  1717 			});
       
  1718 
       
  1719 			api.HeaderTool.combinedList = api.HeaderTool.CombinedList = new api.HeaderTool.CombinedList([
       
  1720 				api.HeaderTool.UploadsList,
       
  1721 				api.HeaderTool.DefaultsList
       
  1722 			]);
       
  1723 		},
       
  1724 
       
  1725 		/**
       
  1726 		 * Returns a new instance of api.HeaderTool.ImageModel based on the currently
       
  1727 		 * saved header image (if any).
       
  1728 		 *
       
  1729 		 * @since 4.2.0
       
  1730 		 *
       
  1731 		 * @returns {Object} Options
       
  1732 		 */
       
  1733 		getInitialHeaderImage: function() {
       
  1734 			if ( ! api.get().header_image || ! api.get().header_image_data || _.contains( [ 'remove-header', 'random-default-image', 'random-uploaded-image' ], api.get().header_image ) ) {
       
  1735 				return new api.HeaderTool.ImageModel();
       
  1736 			}
       
  1737 
       
  1738 			// Get the matching uploaded image object.
       
  1739 			var currentHeaderObject = _.find( _wpCustomizeHeader.uploads, function( imageObj ) {
       
  1740 				return ( imageObj.attachment_id === api.get().header_image_data.attachment_id );
       
  1741 			} );
       
  1742 			// Fall back to raw current header image.
       
  1743 			if ( ! currentHeaderObject ) {
       
  1744 				currentHeaderObject = {
       
  1745 					url: api.get().header_image,
       
  1746 					thumbnail_url: api.get().header_image,
       
  1747 					attachment_id: api.get().header_image_data.attachment_id
       
  1748 				};
       
  1749 			}
       
  1750 
       
  1751 			return new api.HeaderTool.ImageModel({
       
  1752 				header: currentHeaderObject,
       
  1753 				choice: currentHeaderObject.url.split( '/' ).pop()
       
  1754 			});
       
  1755 		},
       
  1756 
       
  1757 		/**
       
  1758 		 * Returns a set of options, computed from the attached image data and
       
  1759 		 * theme-specific data, to be fed to the imgAreaSelect plugin in
       
  1760 		 * wp.media.view.Cropper.
       
  1761 		 *
       
  1762 		 * @param {wp.media.model.Attachment} attachment
       
  1763 		 * @param {wp.media.controller.Cropper} controller
       
  1764 		 * @returns {Object} Options
       
  1765 		 */
       
  1766 		calculateImageSelectOptions: function(attachment, controller) {
       
  1767 			var xInit = parseInt(_wpCustomizeHeader.data.width, 10),
       
  1768 				yInit = parseInt(_wpCustomizeHeader.data.height, 10),
       
  1769 				flexWidth = !! parseInt(_wpCustomizeHeader.data['flex-width'], 10),
       
  1770 				flexHeight = !! parseInt(_wpCustomizeHeader.data['flex-height'], 10),
       
  1771 				ratio, xImg, yImg, realHeight, realWidth,
       
  1772 				imgSelectOptions;
       
  1773 
       
  1774 			realWidth = attachment.get('width');
       
  1775 			realHeight = attachment.get('height');
       
  1776 
       
  1777 			this.headerImage = new api.HeaderTool.ImageModel();
       
  1778 			this.headerImage.set({
       
  1779 				themeWidth: xInit,
       
  1780 				themeHeight: yInit,
       
  1781 				themeFlexWidth: flexWidth,
       
  1782 				themeFlexHeight: flexHeight,
       
  1783 				imageWidth: realWidth,
       
  1784 				imageHeight: realHeight
       
  1785 			});
       
  1786 
       
  1787 			controller.set( 'canSkipCrop', ! this.headerImage.shouldBeCropped() );
       
  1788 
       
  1789 			ratio = xInit / yInit;
       
  1790 			xImg = realWidth;
       
  1791 			yImg = realHeight;
       
  1792 
       
  1793 			if ( xImg / yImg > ratio ) {
       
  1794 				yInit = yImg;
       
  1795 				xInit = yInit * ratio;
       
  1796 			} else {
       
  1797 				xInit = xImg;
       
  1798 				yInit = xInit / ratio;
       
  1799 			}
       
  1800 
       
  1801 			imgSelectOptions = {
       
  1802 				handles: true,
       
  1803 				keys: true,
       
  1804 				instance: true,
       
  1805 				persistent: true,
       
  1806 				imageWidth: realWidth,
       
  1807 				imageHeight: realHeight,
       
  1808 				x1: 0,
       
  1809 				y1: 0,
       
  1810 				x2: xInit,
       
  1811 				y2: yInit
       
  1812 			};
       
  1813 
       
  1814 			if (flexHeight === false && flexWidth === false) {
       
  1815 				imgSelectOptions.aspectRatio = xInit + ':' + yInit;
       
  1816 			}
       
  1817 			if (flexHeight === false ) {
       
  1818 				imgSelectOptions.maxHeight = yInit;
       
  1819 			}
       
  1820 			if (flexWidth === false ) {
       
  1821 				imgSelectOptions.maxWidth = xInit;
       
  1822 			}
       
  1823 
       
  1824 			return imgSelectOptions;
       
  1825 		},
       
  1826 
       
  1827 		/**
       
  1828 		 * Sets up and opens the Media Manager in order to select an image.
       
  1829 		 * Depending on both the size of the image and the properties of the
       
  1830 		 * current theme, a cropping step after selection may be required or
       
  1831 		 * skippable.
       
  1832 		 *
       
  1833 		 * @param {event} event
       
  1834 		 */
       
  1835 		openMedia: function(event) {
       
  1836 			var l10n = _wpMediaViewsL10n;
       
  1837 
       
  1838 			event.preventDefault();
       
  1839 
       
  1840 			this.frame = wp.media({
       
  1841 				button: {
       
  1842 					text: l10n.selectAndCrop,
       
  1843 					close: false
       
  1844 				},
       
  1845 				states: [
       
  1846 					new wp.media.controller.Library({
       
  1847 						title:     l10n.chooseImage,
       
  1848 						library:   wp.media.query({ type: 'image' }),
       
  1849 						multiple:  false,
       
  1850 						date:      false,
       
  1851 						priority:  20,
       
  1852 						suggestedWidth: _wpCustomizeHeader.data.width,
       
  1853 						suggestedHeight: _wpCustomizeHeader.data.height
       
  1854 					}),
       
  1855 					new wp.media.controller.Cropper({
       
  1856 						imgSelectOptions: this.calculateImageSelectOptions
       
  1857 					})
       
  1858 				]
       
  1859 			});
       
  1860 
       
  1861 			this.frame.on('select', this.onSelect, this);
       
  1862 			this.frame.on('cropped', this.onCropped, this);
       
  1863 			this.frame.on('skippedcrop', this.onSkippedCrop, this);
       
  1864 
       
  1865 			this.frame.open();
       
  1866 		},
       
  1867 
       
  1868 		/**
       
  1869 		 * After an image is selected in the media modal,
       
  1870 		 * switch to the cropper state.
       
  1871 		 */
       
  1872 		onSelect: function() {
       
  1873 			this.frame.setState('cropper');
       
  1874 		},
       
  1875 
       
  1876 		/**
       
  1877 		 * After the image has been cropped, apply the cropped image data to the setting.
       
  1878 		 *
       
  1879 		 * @param {object} croppedImage Cropped attachment data.
       
  1880 		 */
       
  1881 		onCropped: function(croppedImage) {
       
  1882 			var url = croppedImage.post_content,
       
  1883 				attachmentId = croppedImage.attachment_id,
       
  1884 				w = croppedImage.width,
       
  1885 				h = croppedImage.height;
       
  1886 			this.setImageFromURL(url, attachmentId, w, h);
       
  1887 		},
       
  1888 
       
  1889 		/**
       
  1890 		 * If cropping was skipped, apply the image data directly to the setting.
       
  1891 		 *
       
  1892 		 * @param {object} selection
       
  1893 		 */
       
  1894 		onSkippedCrop: function(selection) {
       
  1895 			var url = selection.get('url'),
       
  1896 				w = selection.get('width'),
       
  1897 				h = selection.get('height');
       
  1898 			this.setImageFromURL(url, selection.id, w, h);
       
  1899 		},
       
  1900 
       
  1901 		/**
       
  1902 		 * Creates a new wp.customize.HeaderTool.ImageModel from provided
       
  1903 		 * header image data and inserts it into the user-uploaded headers
       
  1904 		 * collection.
       
  1905 		 *
       
  1906 		 * @param {String} url
       
  1907 		 * @param {Number} attachmentId
       
  1908 		 * @param {Number} width
       
  1909 		 * @param {Number} height
       
  1910 		 */
       
  1911 		setImageFromURL: function(url, attachmentId, width, height) {
       
  1912 			var choice, data = {};
       
  1913 
       
  1914 			data.url = url;
       
  1915 			data.thumbnail_url = url;
       
  1916 			data.timestamp = _.now();
       
  1917 
       
  1918 			if (attachmentId) {
       
  1919 				data.attachment_id = attachmentId;
       
  1920 			}
       
  1921 
       
  1922 			if (width) {
       
  1923 				data.width = width;
       
  1924 			}
       
  1925 
       
  1926 			if (height) {
       
  1927 				data.height = height;
       
  1928 			}
       
  1929 
       
  1930 			choice = new api.HeaderTool.ImageModel({
       
  1931 				header: data,
       
  1932 				choice: url.split('/').pop()
       
  1933 			});
       
  1934 			api.HeaderTool.UploadsList.add(choice);
       
  1935 			api.HeaderTool.currentHeader.set(choice.toJSON());
       
  1936 			choice.save();
       
  1937 			choice.importImage();
       
  1938 		},
       
  1939 
       
  1940 		/**
       
  1941 		 * Triggers the necessary events to deselect an image which was set as
       
  1942 		 * the currently selected one.
       
  1943 		 */
       
  1944 		removeImage: function() {
       
  1945 			api.HeaderTool.currentHeader.trigger('hide');
       
  1946 			api.HeaderTool.CombinedList.trigger('control:removeImage');
       
  1947 		}
       
  1948 
       
  1949 	});
       
  1950 
       
  1951 	/**
       
  1952 	 * wp.customize.ThemeControl
       
  1953 	 *
       
  1954 	 * @constructor
       
  1955 	 * @augments wp.customize.Control
       
  1956 	 * @augments wp.customize.Class
       
  1957 	 */
       
  1958 	api.ThemeControl = api.Control.extend({
       
  1959 
       
  1960 		touchDrag: false,
       
  1961 		isRendered: false,
       
  1962 
       
  1963 		/**
       
  1964 		 * Defer rendering the theme control until the section is displayed.
       
  1965 		 *
       
  1966 		 * @since 4.2.0
       
  1967 		 */
       
  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() {
       
  1991 			var control = this;
       
  1992 
       
  1993 			control.container.on( 'touchmove', '.theme', function() {
       
  1994 				control.touchDrag = true;
       
  1995 			});
       
  1996 
       
  1997 			// Bind details view trigger.
       
  1998 			control.container.on( 'click keydown touchend', '.theme', function( event ) {
       
  1999 				if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
       
  2000 					return;
       
  2001 				}
       
  2002 
       
  2003 				// Bail if the user scrolled on a touch device.
       
  2004 				if ( control.touchDrag === true ) {
       
  2005 					return control.touchDrag = false;
       
  2006 				}
       
  2007 
       
  2008 				// Prevent the modal from showing when the user clicks the action button.
       
  2009 				if ( $( event.target ).is( '.theme-actions .button' ) ) {
       
  2010 					return;
       
  2011 				}
       
  2012 
       
  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
       
  2026 
       
  2027 				api.section( control.section() ).showDetails( control.params.theme );
       
  2028 			});
       
  2029 
       
  2030 			control.container.on( 'render-screenshot', function() {
       
  2031 				var $screenshot = $( this ).find( 'img' ),
       
  2032 					source = $screenshot.data( 'src' );
       
  2033 
       
  2034 				if ( source ) {
       
  2035 					$screenshot.attr( 'src', source );
       
  2036 				}
       
  2037 			});
       
  2038 		},
       
  2039 
       
  2040 		/**
       
  2041 		 * Show or hide the theme based on the presence of the term in the title, description, and author.
       
  2042 		 *
       
  2043 		 * @since 4.2.0
       
  2044 		 */
       
  2045 		filter: function( term ) {
       
  2046 			var control = this,
       
  2047 				haystack = control.params.theme.name + ' ' +
       
  2048 					control.params.theme.description + ' ' +
       
  2049 					control.params.theme.tags + ' ' +
       
  2050 					control.params.theme.author;
       
  2051 			haystack = haystack.toLowerCase().replace( '-', ' ' );
       
  2052 			if ( -1 !== haystack.search( term ) ) {
       
  2053 				control.activate();
       
  2054 			} else {
       
  2055 				control.deactivate();
       
  2056 			}
       
  2057 		}
       
  2058 	});
       
  2059 
   312 	// Change objects contained within the main customize object to Settings.
  2060 	// Change objects contained within the main customize object to Settings.
   313 	api.defaultConstructor = api.Setting;
  2061 	api.defaultConstructor = api.Setting;
   314 
  2062 
   315 	// Create the collection of Control objects.
  2063 	// Create the collections for Controls, Sections and Panels.
   316 	api.control = new api.Values({ defaultConstructor: api.Control });
  2064 	api.control = new api.Values({ defaultConstructor: api.Control });
   317 
  2065 	api.section = new api.Values({ defaultConstructor: api.Section });
       
  2066 	api.panel = new api.Values({ defaultConstructor: api.Panel });
       
  2067 
       
  2068 	/**
       
  2069 	 * @class
       
  2070 	 * @augments wp.customize.Messenger
       
  2071 	 * @augments wp.customize.Class
       
  2072 	 * @mixes wp.customize.Events
       
  2073 	 */
   318 	api.PreviewFrame = api.Messenger.extend({
  2074 	api.PreviewFrame = api.Messenger.extend({
   319 		sensitivity: 2000,
  2075 		sensitivity: 2000,
   320 
  2076 
   321 		initialize: function( params, options ) {
  2077 		initialize: function( params, options ) {
   322 			var deferred = $.Deferred(),
  2078 			var deferred = $.Deferred();
   323 				self     = this;
       
   324 
  2079 
   325 			// This is the promise object.
  2080 			// This is the promise object.
   326 			deferred.promise( this );
  2081 			deferred.promise( this );
   327 
  2082 
   328 			this.container = params.container;
  2083 			this.container = params.container;
   342 		run: function( deferred ) {
  2097 		run: function( deferred ) {
   343 			var self   = this,
  2098 			var self   = this,
   344 				loaded = false,
  2099 				loaded = false,
   345 				ready  = false;
  2100 				ready  = false;
   346 
  2101 
   347 			if ( this._ready )
  2102 			if ( this._ready ) {
   348 				this.unbind( 'ready', this._ready );
  2103 				this.unbind( 'ready', this._ready );
       
  2104 			}
   349 
  2105 
   350 			this._ready = function() {
  2106 			this._ready = function() {
   351 				ready = true;
  2107 				ready = true;
   352 
  2108 
   353 				if ( loaded )
  2109 				if ( loaded ) {
   354 					deferred.resolveWith( self );
  2110 					deferred.resolveWith( self );
       
  2111 				}
   355 			};
  2112 			};
   356 
  2113 
   357 			this.bind( 'ready', this._ready );
  2114 			this.bind( 'ready', this._ready );
       
  2115 
       
  2116 			this.bind( 'ready', function ( data ) {
       
  2117 
       
  2118 				this.container.addClass( 'iframe-ready' );
       
  2119 
       
  2120 				if ( ! data ) {
       
  2121 					return;
       
  2122 				}
       
  2123 
       
  2124 				/*
       
  2125 				 * Walk over all panels, sections, and controls and set their
       
  2126 				 * respective active states to true if the preview explicitly
       
  2127 				 * indicates as such.
       
  2128 				 */
       
  2129 				var constructs = {
       
  2130 					panel: data.activePanels,
       
  2131 					section: data.activeSections,
       
  2132 					control: data.activeControls
       
  2133 				};
       
  2134 				_( constructs ).each( function ( activeConstructs, type ) {
       
  2135 					api[ type ].each( function ( construct, id ) {
       
  2136 						var active = !! ( activeConstructs && activeConstructs[ id ] );
       
  2137 						construct.active( active );
       
  2138 					} );
       
  2139 				} );
       
  2140 			} );
   358 
  2141 
   359 			this.request = $.ajax( this.previewUrl(), {
  2142 			this.request = $.ajax( this.previewUrl(), {
   360 				type: 'POST',
  2143 				type: 'POST',
   361 				data: this.query,
  2144 				data: this.query,
   362 				xhrFields: {
  2145 				xhrFields: {
   373 					signature = self.signature,
  2156 					signature = self.signature,
   374 					index;
  2157 					index;
   375 
  2158 
   376 				// Check if the location response header differs from the current URL.
  2159 				// Check if the location response header differs from the current URL.
   377 				// If so, the request was redirected; try loading the requested page.
  2160 				// If so, the request was redirected; try loading the requested page.
   378 				if ( location && location != self.previewUrl() ) {
  2161 				if ( location && location !== self.previewUrl() ) {
   379 					deferred.rejectWith( self, [ 'redirect', location ] );
  2162 					deferred.rejectWith( self, [ 'redirect', location ] );
   380 					return;
  2163 					return;
   381 				}
  2164 				}
   382 
  2165 
   383 				// Check if the user is not logged in.
  2166 				// Check if the user is not logged in.
   401 
  2184 
   402 				// Strip the signature from the request.
  2185 				// Strip the signature from the request.
   403 				response = response.slice( 0, index ) + response.slice( index + signature.length );
  2186 				response = response.slice( 0, index ) + response.slice( index + signature.length );
   404 
  2187 
   405 				// Create the iframe and inject the html content.
  2188 				// Create the iframe and inject the html content.
   406 				self.iframe = $('<iframe />').appendTo( self.container );
  2189 				self.iframe = $( '<iframe />', { 'title': api.l10n.previewIframeTitle } ).appendTo( self.container );
   407 
  2190 
   408 				// Bind load event after the iframe has been added to the page;
  2191 				// Bind load event after the iframe has been added to the page;
   409 				// otherwise it will fire when injected into the DOM.
  2192 				// otherwise it will fire when injected into the DOM.
   410 				self.iframe.one( 'load', function() {
  2193 				self.iframe.one( 'load', function() {
   411 					loaded = true;
  2194 					loaded = true;
   433 
  2216 
   434 			reject = function() {
  2217 			reject = function() {
   435 				deferred.rejectWith( self, [ 'logged out' ] );
  2218 				deferred.rejectWith( self, [ 'logged out' ] );
   436 			};
  2219 			};
   437 
  2220 
   438 			if ( this.triedLogin )
  2221 			if ( this.triedLogin ) {
   439 				return reject();
  2222 				return reject();
       
  2223 			}
   440 
  2224 
   441 			// Check if we have an admin cookie.
  2225 			// Check if we have an admin cookie.
   442 			$.get( api.settings.url.ajax, {
  2226 			$.get( api.settings.url.ajax, {
   443 				action: 'logged-in'
  2227 				action: 'logged-in'
   444 			}).fail( reject ).done( function( response ) {
  2228 			}).fail( reject ).done( function( response ) {
   445 				var iframe;
  2229 				var iframe;
   446 
  2230 
   447 				if ( '1' !== response )
  2231 				if ( '1' !== response ) {
   448 					reject();
  2232 					reject();
   449 
  2233 				}
   450 				iframe = $('<iframe src="' + self.previewUrl() + '" />').hide();
  2234 
       
  2235 				iframe = $( '<iframe />', { 'src': self.previewUrl(), 'title': api.l10n.previewIframeTitle } ).hide();
   451 				iframe.appendTo( self.container );
  2236 				iframe.appendTo( self.container );
   452 				iframe.load( function() {
  2237 				iframe.load( function() {
   453 					self.triedLogin = true;
  2238 					self.triedLogin = true;
   454 
  2239 
   455 					iframe.remove();
  2240 					iframe.remove();
   471 		}
  2256 		}
   472 	});
  2257 	});
   473 
  2258 
   474 	(function(){
  2259 	(function(){
   475 		var uuid = 0;
  2260 		var uuid = 0;
       
  2261 		/**
       
  2262 		 * Create a universally unique identifier.
       
  2263 		 *
       
  2264 		 * @return {int}
       
  2265 		 */
   476 		api.PreviewFrame.uuid = function() {
  2266 		api.PreviewFrame.uuid = function() {
   477 			return 'preview-' + uuid++;
  2267 			return 'preview-' + uuid++;
   478 		};
  2268 		};
   479 	}());
  2269 	}());
   480 
  2270 
       
  2271 	/**
       
  2272 	 * Set the document title of the customizer.
       
  2273 	 *
       
  2274 	 * @since 4.1.0
       
  2275 	 *
       
  2276 	 * @param {string} documentTitle
       
  2277 	 */
       
  2278 	api.setDocumentTitle = function ( documentTitle ) {
       
  2279 		var tmpl, title;
       
  2280 		tmpl = api.settings.documentTitleTmpl;
       
  2281 		title = tmpl.replace( '%s', documentTitle );
       
  2282 		document.title = title;
       
  2283 		api.trigger( 'title', title );
       
  2284 	};
       
  2285 
       
  2286 	/**
       
  2287 	 * @class
       
  2288 	 * @augments wp.customize.Messenger
       
  2289 	 * @augments wp.customize.Class
       
  2290 	 * @mixes wp.customize.Events
       
  2291 	 */
   481 	api.Previewer = api.Messenger.extend({
  2292 	api.Previewer = api.Messenger.extend({
   482 		refreshBuffer: 250,
  2293 		refreshBuffer: 250,
   483 
  2294 
   484 		/**
  2295 		/**
   485 		 * Requires params:
  2296 		 * Requires params:
   486 		 *  - container  - a selector or jQuery element
  2297 		 *  - container  - a selector or jQuery element
   487 		 *  - previewUrl - the URL of preview frame
  2298 		 *  - previewUrl - the URL of preview frame
   488 		 */
  2299 		 */
   489 		initialize: function( params, options ) {
  2300 		initialize: function( params, options ) {
   490 			var self = this,
  2301 			var self = this,
   491 				rscheme = /^https?/,
  2302 				rscheme = /^https?/;
   492 				url;
       
   493 
  2303 
   494 			$.extend( this, options || {} );
  2304 			$.extend( this, options || {} );
       
  2305 			this.deferred = {
       
  2306 				active: $.Deferred()
       
  2307 			};
   495 
  2308 
   496 			/*
  2309 			/*
   497 			 * Wrap this.refresh to prevent it from hammering the servers:
  2310 			 * Wrap this.refresh to prevent it from hammering the servers:
   498 			 *
  2311 			 *
   499 			 * If refresh is called once and no other refresh requests are
  2312 			 * If refresh is called once and no other refresh requests are
   541 			});
  2354 			});
   542 
  2355 
   543 			// Limit the URL to internal, front-end links.
  2356 			// Limit the URL to internal, front-end links.
   544 			//
  2357 			//
   545 			// If the frontend and the admin are served from the same domain, load the
  2358 			// If the frontend and the admin are served from the same domain, load the
   546 			// preview over ssl if the customizer is being loaded over ssl. This avoids
  2359 			// preview over ssl if the Customizer is being loaded over ssl. This avoids
   547 			// insecure content warnings. This is not attempted if the admin and frontend
  2360 			// insecure content warnings. This is not attempted if the admin and frontend
   548 			// are on different domains to avoid the case where the frontend doesn't have
  2361 			// are on different domains to avoid the case where the frontend doesn't have
   549 			// ssl certs.
  2362 			// ssl certs.
   550 
  2363 
   551 			this.add( 'previewUrl', params.previewUrl ).setter( function( to ) {
  2364 			this.add( 'previewUrl', params.previewUrl ).setter( function( to ) {
   586 				this.scroll = distance;
  2399 				this.scroll = distance;
   587 			});
  2400 			});
   588 
  2401 
   589 			// Update the URL when the iframe sends a URL message.
  2402 			// Update the URL when the iframe sends a URL message.
   590 			this.bind( 'url', this.previewUrl );
  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 			} );
   591 		},
  2409 		},
   592 
  2410 
   593 		query: function() {},
  2411 		query: function() {},
   594 
  2412 
   595 		abort: function() {
  2413 		abort: function() {
   599 			}
  2417 			}
   600 		},
  2418 		},
   601 
  2419 
   602 		refresh: function() {
  2420 		refresh: function() {
   603 			var self = this;
  2421 			var self = this;
       
  2422 
       
  2423 			// Display loading indicator
       
  2424 			this.send( 'loading-initiated' );
   604 
  2425 
   605 			this.abort();
  2426 			this.abort();
   606 
  2427 
   607 			this.loading = new api.PreviewFrame({
  2428 			this.loading = new api.PreviewFrame({
   608 				url:        this.url(),
  2429 				url:        this.url(),
   621 					delete self.loading;
  2442 					delete self.loading;
   622 
  2443 
   623 					self.targetWindow( this.targetWindow() );
  2444 					self.targetWindow( this.targetWindow() );
   624 					self.channel( this.channel() );
  2445 					self.channel( this.channel() );
   625 
  2446 
       
  2447 					self.deferred.active.resolve();
   626 					self.send( 'active' );
  2448 					self.send( 'active' );
   627 				});
  2449 				});
   628 
  2450 
   629 				this.send( 'sync', {
  2451 				this.send( 'sync', {
   630 					scroll:   self.scroll,
  2452 					scroll:   self.scroll,
   631 					settings: api.get()
  2453 					settings: api.get()
   632 				});
  2454 				});
   633 			});
  2455 			});
   634 
  2456 
   635 			this.loading.fail( function( reason, location ) {
  2457 			this.loading.fail( function( reason, location ) {
   636 				if ( 'redirect' === reason && location )
  2458 				self.send( 'loading-failed' );
       
  2459 				if ( 'redirect' === reason && location ) {
   637 					self.previewUrl( location );
  2460 					self.previewUrl( location );
       
  2461 				}
   638 
  2462 
   639 				if ( 'logged out' === reason ) {
  2463 				if ( 'logged out' === reason ) {
   640 					if ( self.preview ) {
  2464 					if ( self.preview ) {
   641 						self.preview.destroy();
  2465 						self.preview.destroy();
   642 						delete self.preview;
  2466 						delete self.preview;
   643 					}
  2467 					}
   644 
  2468 
   645 					self.login().done( self.refresh );
  2469 					self.login().done( self.refresh );
   646 				}
  2470 				}
   647 
  2471 
   648 				if ( 'cheatin' === reason )
  2472 				if ( 'cheatin' === reason ) {
   649 					self.cheatin();
  2473 					self.cheatin();
       
  2474 				}
   650 			});
  2475 			});
   651 		},
  2476 		},
   652 
  2477 
   653 		login: function() {
  2478 		login: function() {
   654 			var previewer = this,
  2479 			var previewer = this,
   663 			messenger = new api.Messenger({
  2488 			messenger = new api.Messenger({
   664 				channel: 'login',
  2489 				channel: 'login',
   665 				url:     api.settings.url.login
  2490 				url:     api.settings.url.login
   666 			});
  2491 			});
   667 
  2492 
   668 			iframe = $('<iframe src="' + api.settings.url.login + '" />').appendTo( this.container );
  2493 			iframe = $( '<iframe />', { 'src': api.settings.url.login, 'title': api.l10n.loginIframeTitle } ).appendTo( this.container );
   669 
  2494 
   670 			messenger.targetWindow( iframe[0].contentWindow );
  2495 			messenger.targetWindow( iframe[0].contentWindow );
   671 
  2496 
   672 			messenger.bind( 'login', function() {
  2497 			messenger.bind( 'login', function () {
   673 				iframe.remove();
  2498 				var refreshNonces = previewer.refreshNonces();
   674 				messenger.destroy();
  2499 
   675 				delete previewer._login;
  2500 				refreshNonces.always( function() {
   676 				deferred.resolve();
  2501 					iframe.remove();
       
  2502 					messenger.destroy();
       
  2503 					delete previewer._login;
       
  2504 				});
       
  2505 
       
  2506 				refreshNonces.done( function() {
       
  2507 					deferred.resolve();
       
  2508 				});
       
  2509 
       
  2510 				refreshNonces.fail( function() {
       
  2511 					previewer.cheatin();
       
  2512 					deferred.reject();
       
  2513 				});
   677 			});
  2514 			});
   678 
  2515 
   679 			return this._login;
  2516 			return this._login;
   680 		},
  2517 		},
   681 
  2518 
   682 		cheatin: function() {
  2519 		cheatin: function() {
   683 			$( document.body ).empty().addClass('cheatin').append( '<p>' + api.l10n.cheatin + '</p>' );
  2520 			$( document.body ).empty().addClass('cheatin').append( '<p>' + api.l10n.cheatin + '</p>' );
       
  2521 		},
       
  2522 
       
  2523 		refreshNonces: function() {
       
  2524 			var request, deferred = $.Deferred();
       
  2525 
       
  2526 			deferred.promise();
       
  2527 
       
  2528 			request = wp.ajax.post( 'customize_refresh_nonces', {
       
  2529 				wp_customize: 'on',
       
  2530 				theme: api.settings.theme.stylesheet
       
  2531 			});
       
  2532 
       
  2533 			request.done( function( response ) {
       
  2534 				api.trigger( 'nonce-refresh', response );
       
  2535 				deferred.resolve();
       
  2536 			});
       
  2537 
       
  2538 			request.fail( function() {
       
  2539 				deferred.reject();
       
  2540 			});
       
  2541 
       
  2542 			return deferred;
   684 		}
  2543 		}
   685 	});
  2544 	});
   686 
  2545 
   687 	/* =====================================================================
       
   688 	 * Ready.
       
   689 	 * ===================================================================== */
       
   690 
       
   691 	api.controlConstructor = {
  2546 	api.controlConstructor = {
   692 		color:  api.ColorControl,
  2547 		color:      api.ColorControl,
   693 		upload: api.UploadControl,
  2548 		media:      api.MediaControl,
   694 		image:  api.ImageControl
  2549 		upload:     api.UploadControl,
       
  2550 		image:      api.ImageControl,
       
  2551 		header:     api.HeaderControl,
       
  2552 		background: api.BackgroundControl,
       
  2553 		theme:      api.ThemeControl
       
  2554 	};
       
  2555 	api.panelConstructor = {};
       
  2556 	api.sectionConstructor = {
       
  2557 		themes: api.ThemesSection
   695 	};
  2558 	};
   696 
  2559 
   697 	$( function() {
  2560 	$( function() {
   698 		api.settings = window._wpCustomizeSettings;
  2561 		api.settings = window._wpCustomizeSettings;
   699 		api.l10n = window._wpCustomizeControlsL10n;
  2562 		api.l10n = window._wpCustomizeControlsL10n;
   700 
  2563 
   701 		// Check if we can run the customizer.
  2564 		// Check if we can run the Customizer.
   702 		if ( ! api.settings )
  2565 		if ( ! api.settings ) {
   703 			return;
  2566 			return;
       
  2567 		}
   704 
  2568 
   705 		// Redirect to the fallback preview if any incompatibilities are found.
  2569 		// Redirect to the fallback preview if any incompatibilities are found.
   706 		if ( ! $.support.postMessage || ( ! $.support.cors && api.settings.isCrossDomain ) )
  2570 		if ( ! $.support.postMessage || ( ! $.support.cors && api.settings.isCrossDomain ) )
   707 			return window.location = api.settings.url.fallback;
  2571 			return window.location = api.settings.url.fallback;
   708 
  2572 
   709 		var body = $( document.body ),
  2573 		var parent, topFocus,
   710 			overlay = body.children('.wp-full-overlay'),
  2574 			body = $( document.body ),
   711 			query, previewer, parent;
  2575 			overlay = body.children( '.wp-full-overlay' ),
   712 
  2576 			title = $( '#customize-info .theme-name.site-title' ),
   713 		// Prevent the form from saving when enter is pressed.
  2577 			closeBtn = $( '.customize-controls-close' ),
       
  2578 			saveBtn = $( '#save' );
       
  2579 
       
  2580 		// Prevent the form from saving when enter is pressed on an input or select element.
   714 		$('#customize-controls').on( 'keydown', function( e ) {
  2581 		$('#customize-controls').on( 'keydown', function( e ) {
   715 			if ( $( e.target ).is('textarea') )
  2582 			var isEnter = ( 13 === e.which ),
       
  2583 				$el = $( e.target );
       
  2584 
       
  2585 			if ( isEnter && ( $el.is( 'input:not([type=button])' ) || $el.is( 'select' ) ) ) {
       
  2586 				e.preventDefault();
       
  2587 			}
       
  2588 		});
       
  2589 
       
  2590 		// Expand/Collapse the main customizer customize info.
       
  2591 		$( '#customize-info' ).find( '> .accordion-section-title' ).on( 'click keydown', function( event ) {
       
  2592 			if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
   716 				return;
  2593 				return;
   717 
  2594 			}
   718 			if ( 13 === e.which ) // Enter
  2595 			event.preventDefault(); // Keep this AFTER the key filter above
   719 				e.preventDefault();
  2596 
       
  2597 			var section = $( this ).parent(),
       
  2598 				content = section.find( '.accordion-section-content:first' );
       
  2599 
       
  2600 			if ( section.hasClass( 'cannot-expand' ) ) {
       
  2601 				return;
       
  2602 			}
       
  2603 
       
  2604 			if ( section.hasClass( 'open' ) ) {
       
  2605 				section.toggleClass( 'open' );
       
  2606 				content.slideUp( api.Panel.prototype.defaultExpandedArguments.duration );
       
  2607 			} else {
       
  2608 				content.slideDown( api.Panel.prototype.defaultExpandedArguments.duration );
       
  2609 				section.toggleClass( 'open' );
       
  2610 			}
   720 		});
  2611 		});
   721 
  2612 
   722 		// Initialize Previewer
  2613 		// Initialize Previewer
   723 		previewer = new api.Previewer({
  2614 		api.previewer = new api.Previewer({
   724 			container:   '#customize-preview',
  2615 			container:   '#customize-preview',
   725 			form:        '#customize-controls',
  2616 			form:        '#customize-controls',
   726 			previewUrl:  api.settings.url.preview,
  2617 			previewUrl:  api.settings.url.preview,
   727 			allowedUrls: api.settings.url.allowed,
  2618 			allowedUrls: api.settings.url.allowed,
   728 			signature:   'WP_CUSTOMIZER_SIGNATURE'
  2619 			signature:   'WP_CUSTOMIZER_SIGNATURE'
   729 		}, {
  2620 		}, {
   730 
  2621 
   731 			nonce: api.settings.nonce,
  2622 			nonce: api.settings.nonce,
   732 
  2623 
   733 			query: function() {
  2624 			query: function() {
       
  2625 				var dirtyCustomized = {};
       
  2626 				api.each( function ( value, key ) {
       
  2627 					if ( value._dirty ) {
       
  2628 						dirtyCustomized[ key ] = value();
       
  2629 					}
       
  2630 				} );
       
  2631 
   734 				return {
  2632 				return {
   735 					wp_customize: 'on',
  2633 					wp_customize: 'on',
   736 					theme:        api.settings.theme.stylesheet,
  2634 					theme:      api.settings.theme.stylesheet,
   737 					customized:   JSON.stringify( api.get() ),
  2635 					customized: JSON.stringify( dirtyCustomized ),
   738 					nonce:        this.nonce.preview
  2636 					nonce:      this.nonce.preview
   739 				};
  2637 				};
   740 			},
  2638 			},
   741 
  2639 
   742 			save: function() {
  2640 			save: function() {
   743 				var self  = this,
  2641 				var self = this,
   744 					query = $.extend( this.query(), {
  2642 					processing = api.state( 'processing' ),
   745 						action: 'customize_save',
  2643 					submitWhenDoneProcessing,
   746 						nonce:  this.nonce.save
  2644 					submit;
   747 					}),
  2645 
   748 					request = $.post( api.settings.url.ajax, query );
  2646 				body.addClass( 'saving' );
   749 
  2647 
   750 				api.trigger( 'save', request );
  2648 				submit = function () {
   751 
  2649 					var request, query;
   752 				body.addClass('saving');
  2650 					query = $.extend( self.query(), {
   753 
  2651 						nonce:  self.nonce.save
   754 				request.always( function() {
  2652 					} );
   755 					body.removeClass('saving');
  2653 					request = wp.ajax.post( 'customize_save', query );
   756 				});
  2654 
   757 
  2655 					api.trigger( 'save', request );
   758 				request.done( function( response ) {
  2656 
   759 					// Check if the user is logged out.
  2657 					request.always( function () {
   760 					if ( '0' === response ) {
  2658 						body.removeClass( 'saving' );
   761 						self.preview.iframe.hide();
  2659 					} );
   762 						self.login().done( function() {
  2660 
   763 							self.save();
  2661 					request.fail( function ( response ) {
   764 							self.preview.iframe.show();
  2662 						if ( '0' === response ) {
   765 						});
  2663 							response = 'not_logged_in';
   766 						return;
  2664 						} else if ( '-1' === response ) {
   767 					}
  2665 							// Back-compat in case any other check_ajax_referer() call is dying
   768 
  2666 							response = 'invalid_nonce';
   769 					// Check for cheaters.
  2667 						}
   770 					if ( '-1' === response ) {
  2668 
   771 						self.cheatin();
  2669 						if ( 'invalid_nonce' === response ) {
   772 						return;
  2670 							self.cheatin();
   773 					}
  2671 						} else if ( 'not_logged_in' === response ) {
   774 
  2672 							self.preview.iframe.hide();
   775 					api.trigger( 'saved' );
  2673 							self.login().done( function() {
   776 				});
  2674 								self.save();
       
  2675 								self.preview.iframe.show();
       
  2676 							} );
       
  2677 						}
       
  2678 						api.trigger( 'error', response );
       
  2679 					} );
       
  2680 
       
  2681 					request.done( function( response ) {
       
  2682 						// Clear setting dirty states
       
  2683 						api.each( function ( value ) {
       
  2684 							value._dirty = false;
       
  2685 						} );
       
  2686 
       
  2687 						api.trigger( 'saved', response );
       
  2688 					} );
       
  2689 				};
       
  2690 
       
  2691 				if ( 0 === processing() ) {
       
  2692 					submit();
       
  2693 				} else {
       
  2694 					submitWhenDoneProcessing = function () {
       
  2695 						if ( 0 === processing() ) {
       
  2696 							api.state.unbind( 'change', submitWhenDoneProcessing );
       
  2697 							submit();
       
  2698 						}
       
  2699 					};
       
  2700 					api.state.bind( 'change', submitWhenDoneProcessing );
       
  2701 				}
       
  2702 
   777 			}
  2703 			}
   778 		});
  2704 		});
   779 
  2705 
   780 		// Refresh the nonces if the preview sends updated nonces over.
  2706 		// Refresh the nonces if the preview sends updated nonces over.
   781  		previewer.bind( 'nonce', function( nonce ) {
  2707 		api.previewer.bind( 'nonce', function( nonce ) {
   782  			$.extend( this.nonce, nonce );
  2708 			$.extend( this.nonce, nonce );
   783  		});
  2709 		});
   784 
  2710 
       
  2711 		// Refresh the nonces if login sends updated nonces over.
       
  2712 		api.bind( 'nonce-refresh', function( nonce ) {
       
  2713 			$.extend( api.settings.nonce, nonce );
       
  2714 			$.extend( api.previewer.nonce, nonce );
       
  2715 		});
       
  2716 
       
  2717 		// Create Settings
   785 		$.each( api.settings.settings, function( id, data ) {
  2718 		$.each( api.settings.settings, function( id, data ) {
   786 			api.create( id, id, data.value, {
  2719 			api.create( id, id, data.value, {
   787 				transport: data.transport,
  2720 				transport: data.transport,
   788 				previewer: previewer
  2721 				previewer: api.previewer,
       
  2722 				dirty: !! data.dirty
   789 			} );
  2723 			} );
   790 		});
  2724 		});
   791 
  2725 
       
  2726 		// Create Panels
       
  2727 		$.each( api.settings.panels, function ( id, data ) {
       
  2728 			var constructor = api.panelConstructor[ data.type ] || api.Panel,
       
  2729 				panel;
       
  2730 
       
  2731 			panel = new constructor( id, {
       
  2732 				params: data
       
  2733 			} );
       
  2734 			api.panel.add( id, panel );
       
  2735 		});
       
  2736 
       
  2737 		// Create Sections
       
  2738 		$.each( api.settings.sections, function ( id, data ) {
       
  2739 			var constructor = api.sectionConstructor[ data.type ] || api.Section,
       
  2740 				section;
       
  2741 
       
  2742 			section = new constructor( id, {
       
  2743 				params: data
       
  2744 			} );
       
  2745 			api.section.add( id, section );
       
  2746 		});
       
  2747 
       
  2748 		// Create Controls
   792 		$.each( api.settings.controls, function( id, data ) {
  2749 		$.each( api.settings.controls, function( id, data ) {
   793 			var constructor = api.controlConstructor[ data.type ] || api.Control,
  2750 			var constructor = api.controlConstructor[ data.type ] || api.Control,
   794 				control;
  2751 				control;
   795 
  2752 
   796 			control = api.control.add( id, new constructor( id, {
  2753 			control = new constructor( id, {
   797 				params: data,
  2754 				params: data,
   798 				previewer: previewer
  2755 				previewer: api.previewer
   799 			} ) );
  2756 			} );
       
  2757 			api.control.add( id, control );
   800 		});
  2758 		});
   801 
  2759 
       
  2760 		// Focus the autofocused element
       
  2761 		_.each( [ 'panel', 'section', 'control' ], function ( type ) {
       
  2762 			var instance, id = api.settings.autofocus[ type ];
       
  2763 			if ( id && api[ type ]( id ) ) {
       
  2764 				instance = api[ type ]( id );
       
  2765 				// Wait until the element is embedded in the DOM
       
  2766 				instance.deferred.embedded.done( function () {
       
  2767 					// Wait until the preview has activated and so active panels, sections, controls have been set
       
  2768 					api.previewer.deferred.active.done( function () {
       
  2769 						instance.focus();
       
  2770 					});
       
  2771 				});
       
  2772 			}
       
  2773 		});
       
  2774 
       
  2775 		/**
       
  2776 		 * Sort panels, sections, controls by priorities. Hide empty sections and panels.
       
  2777 		 *
       
  2778 		 * @since 4.1.0
       
  2779 		 */
       
  2780 		api.reflowPaneContents = _.bind( function () {
       
  2781 
       
  2782 			var appendContainer, activeElement, rootContainers, rootNodes = [], wasReflowed = false;
       
  2783 
       
  2784 			if ( document.activeElement ) {
       
  2785 				activeElement = $( document.activeElement );
       
  2786 			}
       
  2787 
       
  2788 			// Sort the sections within each panel
       
  2789 			api.panel.each( function ( panel ) {
       
  2790 				var sections = panel.sections(),
       
  2791 					sectionContainers = _.pluck( sections, 'container' );
       
  2792 				rootNodes.push( panel );
       
  2793 				appendContainer = panel.container.find( 'ul:first' );
       
  2794 				if ( ! api.utils.areElementListsEqual( sectionContainers, appendContainer.children( '[id]' ) ) ) {
       
  2795 					_( sections ).each( function ( section ) {
       
  2796 						appendContainer.append( section.container );
       
  2797 					} );
       
  2798 					wasReflowed = true;
       
  2799 				}
       
  2800 			} );
       
  2801 
       
  2802 			// Sort the controls within each section
       
  2803 			api.section.each( function ( section ) {
       
  2804 				var controls = section.controls(),
       
  2805 					controlContainers = _.pluck( controls, 'container' );
       
  2806 				if ( ! section.panel() ) {
       
  2807 					rootNodes.push( section );
       
  2808 				}
       
  2809 				appendContainer = section.container.find( 'ul:first' );
       
  2810 				if ( ! api.utils.areElementListsEqual( controlContainers, appendContainer.children( '[id]' ) ) ) {
       
  2811 					_( controls ).each( function ( control ) {
       
  2812 						appendContainer.append( control.container );
       
  2813 					} );
       
  2814 					wasReflowed = true;
       
  2815 				}
       
  2816 			} );
       
  2817 
       
  2818 			// Sort the root panels and sections
       
  2819 			rootNodes.sort( api.utils.prioritySort );
       
  2820 			rootContainers = _.pluck( rootNodes, 'container' );
       
  2821 			appendContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable
       
  2822 			if ( ! api.utils.areElementListsEqual( rootContainers, appendContainer.children() ) ) {
       
  2823 				_( rootNodes ).each( function ( rootNode ) {
       
  2824 					appendContainer.append( rootNode.container );
       
  2825 				} );
       
  2826 				wasReflowed = true;
       
  2827 			}
       
  2828 
       
  2829 			// Now re-trigger the active Value callbacks to that the panels and sections can decide whether they can be rendered
       
  2830 			api.panel.each( function ( panel ) {
       
  2831 				var value = panel.active();
       
  2832 				panel.active.callbacks.fireWith( panel.active, [ value, value ] );
       
  2833 			} );
       
  2834 			api.section.each( function ( section ) {
       
  2835 				var value = section.active();
       
  2836 				section.active.callbacks.fireWith( section.active, [ value, value ] );
       
  2837 			} );
       
  2838 
       
  2839 			// Restore focus if there was a reflow and there was an active (focused) element
       
  2840 			if ( wasReflowed && activeElement ) {
       
  2841 				activeElement.focus();
       
  2842 			}
       
  2843 		}, api );
       
  2844 		api.bind( 'ready', api.reflowPaneContents );
       
  2845 		api.reflowPaneContents = _.debounce( api.reflowPaneContents, 100 );
       
  2846 		$( [ api.panel, api.section, api.control ] ).each( function ( i, values ) {
       
  2847 			values.bind( 'add', api.reflowPaneContents );
       
  2848 			values.bind( 'change', api.reflowPaneContents );
       
  2849 			values.bind( 'remove', api.reflowPaneContents );
       
  2850 		} );
       
  2851 
   802 		// Check if preview url is valid and load the preview frame.
  2852 		// Check if preview url is valid and load the preview frame.
   803 		if ( previewer.previewUrl() )
  2853 		if ( api.previewer.previewUrl() ) {
   804 			previewer.refresh();
  2854 			api.previewer.refresh();
   805 		else
  2855 		} else {
   806 			previewer.previewUrl( api.settings.url.home );
  2856 			api.previewer.previewUrl( api.settings.url.home );
       
  2857 		}
   807 
  2858 
   808 		// Save and activated states
  2859 		// Save and activated states
   809 		(function() {
  2860 		(function() {
   810 			var state = new api.Values(),
  2861 			var state = new api.Values(),
   811 				saved = state.create('saved'),
  2862 				saved = state.create( 'saved' ),
   812 				activated = state.create('activated');
  2863 				activated = state.create( 'activated' ),
       
  2864 				processing = state.create( 'processing' );
   813 
  2865 
   814 			state.bind( 'change', function() {
  2866 			state.bind( 'change', function() {
   815 				var save = $('#save'),
       
   816 					back = $('.back');
       
   817 
       
   818 				if ( ! activated() ) {
  2867 				if ( ! activated() ) {
   819 					save.val( api.l10n.activate ).prop( 'disabled', false );
  2868 					saveBtn.val( api.l10n.activate ).prop( 'disabled', false );
   820 					back.text( api.l10n.cancel );
  2869 					closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
   821 
  2870 
   822 				} else if ( saved() ) {
  2871 				} else if ( saved() ) {
   823 					save.val( api.l10n.saved ).prop( 'disabled', true );
  2872 					saveBtn.val( api.l10n.saved ).prop( 'disabled', true );
   824 					back.text( api.l10n.close );
  2873 					closeBtn.find( '.screen-reader-text' ).text( api.l10n.close );
   825 
  2874 
   826 				} else {
  2875 				} else {
   827 					save.val( api.l10n.save ).prop( 'disabled', false );
  2876 					saveBtn.val( api.l10n.save ).prop( 'disabled', false );
   828 					back.text( api.l10n.cancel );
  2877 					closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
   829 				}
  2878 				}
   830 			});
  2879 			});
   831 
  2880 
   832 			// Set default states.
  2881 			// Set default states.
   833 			saved( true );
  2882 			saved( true );
   834 			activated( api.settings.theme.active );
  2883 			activated( api.settings.theme.active );
       
  2884 			processing( 0 );
   835 
  2885 
   836 			api.bind( 'change', function() {
  2886 			api.bind( 'change', function() {
   837 				state('saved').set( false );
  2887 				state('saved').set( false );
   838 			});
  2888 			});
   839 
  2889 
   850 			// Expose states to the API.
  2900 			// Expose states to the API.
   851 			api.state = state;
  2901 			api.state = state;
   852 		}());
  2902 		}());
   853 
  2903 
   854 		// Button bindings.
  2904 		// Button bindings.
   855 		$('#save').click( function( event ) {
  2905 		saveBtn.click( function( event ) {
   856 			previewer.save();
  2906 			api.previewer.save();
   857 			event.preventDefault();
  2907 			event.preventDefault();
   858 		}).keydown( function( event ) {
  2908 		}).keydown( function( event ) {
   859 			if ( 9 === event.which ) // tab
  2909 			if ( 9 === event.which ) // tab
   860 				return;
  2910 				return;
   861 			if ( 13 === event.which ) // enter
  2911 			if ( 13 === event.which ) // enter
   862 				previewer.save();
  2912 				api.previewer.save();
   863 			event.preventDefault();
  2913 			event.preventDefault();
   864 		});
  2914 		});
   865 
  2915 
   866 		$('.back').keydown( function( event ) {
  2916 		// Go back to the top-level Customizer accordion.
       
  2917 		$( '#customize-header-actions' ).on( 'click keydown', '.control-panel-back', function( event ) {
       
  2918 			if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
       
  2919 				return;
       
  2920 			}
       
  2921 
       
  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 ) {
   867 			if ( 9 === event.which ) // tab
  2929 			if ( 9 === event.which ) // tab
   868 				return;
  2930 				return;
   869 			if ( 13 === event.which ) // enter
  2931 			if ( 13 === event.which ) // enter
   870 				this.click();
  2932 				this.click();
   871 			event.preventDefault();
  2933 			event.preventDefault();
   872 		});
  2934 		});
   873 
  2935 
   874 		$('.upload-dropzone a.upload').keydown( function( event ) {
       
   875 			if ( 13 === event.which ) // enter
       
   876 				this.click();
       
   877 		});
       
   878 
       
   879 		$('.collapse-sidebar').on( 'click keydown', function( event ) {
  2936 		$('.collapse-sidebar').on( 'click keydown', function( event ) {
   880 			if ( event.type === 'keydown' &&  13 !== event.which ) // enter
  2937 			if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
   881 				return;
  2938 				return;
       
  2939 			}
   882 
  2940 
   883 			overlay.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
  2941 			overlay.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
   884 			event.preventDefault();
  2942 			event.preventDefault();
   885 		});
  2943 		});
       
  2944 
       
  2945 		$( '.customize-controls-preview-toggle' ).on( 'click keydown', function( event ) {
       
  2946 			if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
       
  2947 				return;
       
  2948 			}
       
  2949 
       
  2950 			overlay.toggleClass( 'preview-only' );
       
  2951 			event.preventDefault();
       
  2952 		});
       
  2953 
       
  2954 		// Bind site title display to the corresponding field.
       
  2955 		if ( title.length ) {
       
  2956 			$( '#customize-control-blogname input' ).on( 'input', function() {
       
  2957 				title.text( this.value );
       
  2958 			} );
       
  2959 		}
   886 
  2960 
   887 		// Create a potential postMessage connection with the parent frame.
  2961 		// Create a potential postMessage connection with the parent frame.
   888 		parent = new api.Messenger({
  2962 		parent = new api.Messenger({
   889 			url: api.settings.url.parent,
  2963 			url: api.settings.url.parent,
   890 			channel: 'loader'
  2964 			channel: 'loader'
   891 		});
  2965 		});
   892 
  2966 
   893 		// If we receive a 'back' event, we're inside an iframe.
  2967 		// If we receive a 'back' event, we're inside an iframe.
   894 		// Send any clicks to the 'Return' link to the parent page.
  2968 		// Send any clicks to the 'Return' link to the parent page.
   895 		parent.bind( 'back', function() {
  2969 		parent.bind( 'back', function() {
   896 			$('.back').on( 'click.back', function( event ) {
  2970 			closeBtn.on( 'click.customize-controls-close', function( event ) {
   897 				event.preventDefault();
  2971 				event.preventDefault();
   898 				parent.send( 'close' );
  2972 				parent.send( 'close' );
   899 			});
  2973 			});
   900 		});
  2974 		});
   901 
  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 
   902 		// Pass events through to the parent.
  2986 		// Pass events through to the parent.
   903 		api.bind( 'saved', function() {
  2987 		$.each( [ 'saved', 'change' ], function ( i, event ) {
   904 			parent.send( 'saved' );
  2988 			api.bind( event, function() {
   905 		});
  2989 				parent.send( event );
       
  2990 			});
       
  2991 		} );
   906 
  2992 
   907 		// When activated, let the loader handle redirecting the page.
  2993 		// When activated, let the loader handle redirecting the page.
   908 		// If no loader exists, redirect the page ourselves (if a url exists).
  2994 		// If no loader exists, redirect the page ourselves (if a url exists).
   909 		api.bind( 'activated', function() {
  2995 		api.bind( 'activated', function() {
   910 			if ( parent.targetWindow() )
  2996 			if ( parent.targetWindow() )
   911 				parent.send( 'activated', api.settings.url.activated );
  2997 				parent.send( 'activated', api.settings.url.activated );
   912 			else if ( api.settings.url.activated )
  2998 			else if ( api.settings.url.activated )
   913 				window.location = api.settings.url.activated;
  2999 				window.location = api.settings.url.activated;
   914 		});
  3000 		});
   915 
  3001 
       
  3002 		// Pass titles to the parent
       
  3003 		api.bind( 'title', function( newTitle ) {
       
  3004 			parent.send( 'title', newTitle );
       
  3005 		});
       
  3006 
   916 		// Initialize the connection with the parent frame.
  3007 		// Initialize the connection with the parent frame.
   917 		parent.send( 'ready' );
  3008 		parent.send( 'ready' );
   918 
  3009 
   919 		// Control visibility for default controls
  3010 		// Control visibility for default controls
   920 		$.each({
  3011 		$.each({
   921 			'background_image': {
  3012 			'background_image': {
   922 				controls: [ 'background_repeat', 'background_position_x', 'background_attachment' ],
  3013 				controls: [ 'background_repeat', 'background_position_x', 'background_attachment' ],
   923 				callback: function( to ) { return !! to }
  3014 				callback: function( to ) { return !! to; }
   924 			},
  3015 			},
   925 			'show_on_front': {
  3016 			'show_on_front': {
   926 				controls: [ 'page_on_front', 'page_for_posts' ],
  3017 				controls: [ 'page_on_front', 'page_for_posts' ],
   927 				callback: function( to ) { return 'page' === to }
  3018 				callback: function( to ) { return 'page' === to; }
   928 			},
  3019 			},
   929 			'header_textcolor': {
  3020 			'header_textcolor': {
   930 				controls: [ 'header_textcolor' ],
  3021 				controls: [ 'header_textcolor' ],
   931 				callback: function( to ) { return 'blank' !== to }
  3022 				callback: function( to ) { return 'blank' !== to; }
   932 			}
  3023 			}
   933 		}, function( settingId, o ) {
  3024 		}, function( settingId, o ) {
   934 			api( settingId, function( setting ) {
  3025 			api( settingId, function( setting ) {
   935 				$.each( o.controls, function( i, controlId ) {
  3026 				$.each( o.controls, function( i, controlId ) {
   936 					api.control( controlId, function( control ) {
  3027 					api.control( controlId, function( control ) {
   964 			control.setting.bind( function( to ) {
  3055 			control.setting.bind( function( to ) {
   965 				control.element.set( 'blank' !== to );
  3056 				control.element.set( 'blank' !== to );
   966 			});
  3057 			});
   967 		});
  3058 		});
   968 
  3059 
   969 		// Handle header image data
       
   970 		api.control( 'header_image', function( control ) {
       
   971 			control.setting.bind( function( to ) {
       
   972 				if ( to === control.params.removed )
       
   973 					control.settings.data.set( false );
       
   974 			});
       
   975 
       
   976 			control.library.on( 'click', 'a', function( event ) {
       
   977 				control.settings.data.set( $(this).data('customizeHeaderImageData') );
       
   978 			});
       
   979 
       
   980 			control.uploader.success = function( attachment ) {
       
   981 				var data;
       
   982 
       
   983 				api.ImageControl.prototype.success.call( control, attachment );
       
   984 
       
   985 				data = {
       
   986 					attachment_id: attachment.get('id'),
       
   987 					url:           attachment.get('url'),
       
   988 					thumbnail_url: attachment.get('url'),
       
   989 					height:        attachment.get('height'),
       
   990 					width:         attachment.get('width')
       
   991 				};
       
   992 
       
   993 				attachment.element.data( 'customizeHeaderImageData', data );
       
   994 				control.settings.data.set( data );
       
   995 			};
       
   996 		});
       
   997 
       
   998 		api.trigger( 'ready' );
  3060 		api.trigger( 'ready' );
   999 
  3061 
  1000 		// Make sure left column gets focus
  3062 		// Make sure left column gets focus
  1001 		var topFocus = $('.back');
  3063 		topFocus = closeBtn;
  1002 		topFocus.focus();
  3064 		topFocus.focus();
  1003 		setTimeout(function () {
  3065 		setTimeout(function () {
  1004 			topFocus.focus();
  3066 			topFocus.focus();
  1005 		}, 200);
  3067 		}, 200);
  1006 
  3068