]> scripts.mit.edu Git - autoinstalls/wordpress.git/blob - wp-admin/js/customize-controls.js
WordPress 4.7.2-scripts
[autoinstalls/wordpress.git] / wp-admin / js / customize-controls.js
1 /* global _wpCustomizeHeader, _wpCustomizeBackground, _wpMediaViewsL10n, MediaElementPlayer */
2 (function( exports, $ ){
3         var Container, focus, normalizedTransitionendEventName, api = wp.customize;
4
5         /**
6          * A Customizer Setting.
7          *
8          * A setting is WordPress data (theme mod, option, menu, etc.) that the user can
9          * draft changes to in the Customizer.
10          *
11          * @see PHP class WP_Customize_Setting.
12          *
13          * @class
14          * @augments wp.customize.Value
15          * @augments wp.customize.Class
16          *
17          * @param {object} id                The Setting ID.
18          * @param {object} value             The initial value of the setting.
19          * @param {object} options.previewer The Previewer instance to sync with.
20          * @param {object} options.transport The transport to use for previewing. Supports 'refresh' and 'postMessage'.
21          * @param {object} options.dirty
22          */
23         api.Setting = api.Value.extend({
24                 initialize: function( id, value, options ) {
25                         var setting = this;
26                         api.Value.prototype.initialize.call( setting, value, options );
27
28                         setting.id = id;
29                         setting.transport = setting.transport || 'refresh';
30                         setting._dirty = options.dirty || false;
31                         setting.notifications = new api.Values({ defaultConstructor: api.Notification });
32
33                         // Whenever the setting's value changes, refresh the preview.
34                         setting.bind( setting.preview );
35                 },
36
37                 /**
38                  * Refresh the preview, respective of the setting's refresh policy.
39                  *
40                  * If the preview hasn't sent a keep-alive message and is likely
41                  * disconnected by having navigated to a non-allowed URL, then the
42                  * refresh transport will be forced when postMessage is the transport.
43                  * Note that postMessage does not throw an error when the recipient window
44                  * fails to match the origin window, so using try/catch around the
45                  * previewer.send() call to then fallback to refresh will not work.
46                  *
47                  * @since 3.4.0
48                  * @access public
49                  *
50                  * @returns {void}
51                  */
52                 preview: function() {
53                         var setting = this, transport;
54                         transport = setting.transport;
55
56                         if ( 'postMessage' === transport && ! api.state( 'previewerAlive' ).get() ) {
57                                 transport = 'refresh';
58                         }
59
60                         if ( 'postMessage' === transport ) {
61                                 setting.previewer.send( 'setting', [ setting.id, setting() ] );
62                         } else if ( 'refresh' === transport ) {
63                                 setting.previewer.refresh();
64                         }
65                 },
66
67                 /**
68                  * Find controls associated with this setting.
69                  *
70                  * @since 4.6.0
71                  * @returns {wp.customize.Control[]} Controls associated with setting.
72                  */
73                 findControls: function() {
74                         var setting = this, controls = [];
75                         api.control.each( function( control ) {
76                                 _.each( control.settings, function( controlSetting ) {
77                                         if ( controlSetting.id === setting.id ) {
78                                                 controls.push( control );
79                                         }
80                                 } );
81                         } );
82                         return controls;
83                 }
84         });
85
86         /**
87          * Current change count.
88          *
89          * @since 4.7.0
90          * @type {number}
91          * @protected
92          */
93         api._latestRevision = 0;
94
95         /**
96          * Last revision that was saved.
97          *
98          * @since 4.7.0
99          * @type {number}
100          * @protected
101          */
102         api._lastSavedRevision = 0;
103
104         /**
105          * Latest revisions associated with the updated setting.
106          *
107          * @since 4.7.0
108          * @type {object}
109          * @protected
110          */
111         api._latestSettingRevisions = {};
112
113         /*
114          * Keep track of the revision associated with each updated setting so that
115          * requestChangesetUpdate knows which dirty settings to include. Also, once
116          * ready is triggered and all initial settings have been added, increment
117          * revision for each newly-created initially-dirty setting so that it will
118          * also be included in changeset update requests.
119          */
120         api.bind( 'change', function incrementChangedSettingRevision( setting ) {
121                 api._latestRevision += 1;
122                 api._latestSettingRevisions[ setting.id ] = api._latestRevision;
123         } );
124         api.bind( 'ready', function() {
125                 api.bind( 'add', function incrementCreatedSettingRevision( setting ) {
126                         if ( setting._dirty ) {
127                                 api._latestRevision += 1;
128                                 api._latestSettingRevisions[ setting.id ] = api._latestRevision;
129                         }
130                 } );
131         } );
132
133         /**
134          * Get the dirty setting values.
135          *
136          * @since 4.7.0
137          * @access public
138          *
139          * @param {object} [options] Options.
140          * @param {boolean} [options.unsaved=false] Whether only values not saved yet into a changeset will be returned (differential changes).
141          * @returns {object} Dirty setting values.
142          */
143         api.dirtyValues = function dirtyValues( options ) {
144                 var values = {};
145                 api.each( function( setting ) {
146                         var settingRevision;
147
148                         if ( ! setting._dirty ) {
149                                 return;
150                         }
151
152                         settingRevision = api._latestSettingRevisions[ setting.id ];
153
154                         // Skip including settings that have already been included in the changeset, if only requesting unsaved.
155                         if ( api.state( 'changesetStatus' ).get() && ( options && options.unsaved ) && ( _.isUndefined( settingRevision ) || settingRevision <= api._lastSavedRevision ) ) {
156                                 return;
157                         }
158
159                         values[ setting.id ] = setting.get();
160                 } );
161                 return values;
162         };
163
164         /**
165          * Request updates to the changeset.
166          *
167          * @since 4.7.0
168          * @access public
169          *
170          * @param {object} [changes] Mapping of setting IDs to setting params each normally including a value property, or mapping to null.
171          *                           If not provided, then the changes will still be obtained from unsaved dirty settings.
172          * @returns {jQuery.Promise} Promise resolving with the response data.
173          */
174         api.requestChangesetUpdate = function requestChangesetUpdate( changes ) {
175                 var deferred, request, submittedChanges = {}, data;
176                 deferred = new $.Deferred();
177
178                 if ( changes ) {
179                         _.extend( submittedChanges, changes );
180                 }
181
182                 // Ensure all revised settings (changes pending save) are also included, but not if marked for deletion in changes.
183                 _.each( api.dirtyValues( { unsaved: true } ), function( dirtyValue, settingId ) {
184                         if ( ! changes || null !== changes[ settingId ] ) {
185                                 submittedChanges[ settingId ] = _.extend(
186                                         {},
187                                         submittedChanges[ settingId ] || {},
188                                         { value: dirtyValue }
189                                 );
190                         }
191                 } );
192
193                 // Short-circuit when there are no pending changes.
194                 if ( _.isEmpty( submittedChanges ) ) {
195                         deferred.resolve( {} );
196                         return deferred.promise();
197                 }
198
199                 // Make sure that publishing a changeset waits for all changeset update requests to complete.
200                 api.state( 'processing' ).set( api.state( 'processing' ).get() + 1 );
201                 deferred.always( function() {
202                         api.state( 'processing' ).set( api.state( 'processing' ).get() - 1 );
203                 } );
204
205                 // Allow plugins to attach additional params to the settings.
206                 api.trigger( 'changeset-save', submittedChanges );
207
208                 // Ensure that if any plugins add data to save requests by extending query() that they get included here.
209                 data = api.previewer.query( { excludeCustomizedSaved: true } );
210                 delete data.customized; // Being sent in customize_changeset_data instead.
211                 _.extend( data, {
212                         nonce: api.settings.nonce.save,
213                         customize_theme: api.settings.theme.stylesheet,
214                         customize_changeset_data: JSON.stringify( submittedChanges )
215                 } );
216
217                 request = wp.ajax.post( 'customize_save', data );
218
219                 request.done( function requestChangesetUpdateDone( data ) {
220                         var savedChangesetValues = {};
221
222                         // Ensure that all settings updated subsequently will be included in the next changeset update request.
223                         api._lastSavedRevision = Math.max( api._latestRevision, api._lastSavedRevision );
224
225                         api.state( 'changesetStatus' ).set( data.changeset_status );
226                         deferred.resolve( data );
227                         api.trigger( 'changeset-saved', data );
228
229                         if ( data.setting_validities ) {
230                                 _.each( data.setting_validities, function( validity, settingId ) {
231                                         if ( true === validity && _.isObject( submittedChanges[ settingId ] ) && ! _.isUndefined( submittedChanges[ settingId ].value ) ) {
232                                                 savedChangesetValues[ settingId ] = submittedChanges[ settingId ].value;
233                                         }
234                                 } );
235                         }
236
237                         api.previewer.send( 'changeset-saved', _.extend( {}, data, { saved_changeset_values: savedChangesetValues } ) );
238                 } );
239                 request.fail( function requestChangesetUpdateFail( data ) {
240                         deferred.reject( data );
241                         api.trigger( 'changeset-error', data );
242                 } );
243                 request.always( function( data ) {
244                         if ( data.setting_validities ) {
245                                 api._handleSettingValidities( {
246                                         settingValidities: data.setting_validities
247                                 } );
248                         }
249                 } );
250
251                 return deferred.promise();
252         };
253
254         /**
255          * Watch all changes to Value properties, and bubble changes to parent Values instance
256          *
257          * @since 4.1.0
258          *
259          * @param {wp.customize.Class} instance
260          * @param {Array}              properties  The names of the Value instances to watch.
261          */
262         api.utils.bubbleChildValueChanges = function ( instance, properties ) {
263                 $.each( properties, function ( i, key ) {
264                         instance[ key ].bind( function ( to, from ) {
265                                 if ( instance.parent && to !== from ) {
266                                         instance.parent.trigger( 'change', instance );
267                                 }
268                         } );
269                 } );
270         };
271
272         /**
273          * Expand a panel, section, or control and focus on the first focusable element.
274          *
275          * @since 4.1.0
276          *
277          * @param {Object}   [params]
278          * @param {Function} [params.completeCallback]
279          */
280         focus = function ( params ) {
281                 var construct, completeCallback, focus, focusElement;
282                 construct = this;
283                 params = params || {};
284                 focus = function () {
285                         var focusContainer;
286                         if ( ( construct.extended( api.Panel ) || construct.extended( api.Section ) ) && construct.expanded && construct.expanded() ) {
287                                 focusContainer = construct.contentContainer;
288                         } else {
289                                 focusContainer = construct.container;
290                         }
291
292                         focusElement = focusContainer.find( '.control-focus:first' );
293                         if ( 0 === focusElement.length ) {
294                                 // Note that we can't use :focusable due to a jQuery UI issue. See: https://github.com/jquery/jquery-ui/pull/1583
295                                 focusElement = focusContainer.find( 'input, select, textarea, button, object, a[href], [tabindex]' ).filter( ':visible' ).first();
296                         }
297                         focusElement.focus();
298                 };
299                 if ( params.completeCallback ) {
300                         completeCallback = params.completeCallback;
301                         params.completeCallback = function () {
302                                 focus();
303                                 completeCallback();
304                         };
305                 } else {
306                         params.completeCallback = focus;
307                 }
308
309                 api.state( 'paneVisible' ).set( true );
310                 if ( construct.expand ) {
311                         construct.expand( params );
312                 } else {
313                         params.completeCallback();
314                 }
315         };
316
317         /**
318          * Stable sort for Panels, Sections, and Controls.
319          *
320          * If a.priority() === b.priority(), then sort by their respective params.instanceNumber.
321          *
322          * @since 4.1.0
323          *
324          * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} a
325          * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} b
326          * @returns {Number}
327          */
328         api.utils.prioritySort = function ( a, b ) {
329                 if ( a.priority() === b.priority() && typeof a.params.instanceNumber === 'number' && typeof b.params.instanceNumber === 'number' ) {
330                         return a.params.instanceNumber - b.params.instanceNumber;
331                 } else {
332                         return a.priority() - b.priority();
333                 }
334         };
335
336         /**
337          * Return whether the supplied Event object is for a keydown event but not the Enter key.
338          *
339          * @since 4.1.0
340          *
341          * @param {jQuery.Event} event
342          * @returns {boolean}
343          */
344         api.utils.isKeydownButNotEnterEvent = function ( event ) {
345                 return ( 'keydown' === event.type && 13 !== event.which );
346         };
347
348         /**
349          * Return whether the two lists of elements are the same and are in the same order.
350          *
351          * @since 4.1.0
352          *
353          * @param {Array|jQuery} listA
354          * @param {Array|jQuery} listB
355          * @returns {boolean}
356          */
357         api.utils.areElementListsEqual = function ( listA, listB ) {
358                 var equal = (
359                         listA.length === listB.length && // if lists are different lengths, then naturally they are not equal
360                         -1 === _.indexOf( _.map( // are there any false values in the list returned by map?
361                                 _.zip( listA, listB ), // pair up each element between the two lists
362                                 function ( pair ) {
363                                         return $( pair[0] ).is( pair[1] ); // compare to see if each pair are equal
364                                 }
365                         ), false ) // check for presence of false in map's return value
366                 );
367                 return equal;
368         };
369
370         /**
371          * Return browser supported `transitionend` event name.
372          *
373          * @since 4.7.0
374          *
375          * @returns {string|null} Normalized `transitionend` event name or null if CSS transitions are not supported.
376          */
377         normalizedTransitionendEventName = (function () {
378                 var el, transitions, prop;
379                 el = document.createElement( 'div' );
380                 transitions = {
381                         'transition'      : 'transitionend',
382                         'OTransition'     : 'oTransitionEnd',
383                         'MozTransition'   : 'transitionend',
384                         'WebkitTransition': 'webkitTransitionEnd'
385                 };
386                 prop = _.find( _.keys( transitions ), function( prop ) {
387                         return ! _.isUndefined( el.style[ prop ] );
388                 } );
389                 if ( prop ) {
390                         return transitions[ prop ];
391                 } else {
392                         return null;
393                 }
394         })();
395
396         /**
397          * Base class for Panel and Section.
398          *
399          * @since 4.1.0
400          *
401          * @class
402          * @augments wp.customize.Class
403          */
404         Container = api.Class.extend({
405                 defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
406                 defaultExpandedArguments: { duration: 'fast', completeCallback: $.noop },
407                 containerType: 'container',
408                 defaults: {
409                         title: '',
410                         description: '',
411                         priority: 100,
412                         type: 'default',
413                         content: null,
414                         active: true,
415                         instanceNumber: null
416                 },
417
418                 /**
419                  * @since 4.1.0
420                  *
421                  * @param {string}         id - The ID for the container.
422                  * @param {object}         options - Object containing one property: params.
423                  * @param {object}         options.params - Object containing the following properties.
424                  * @param {string}         options.params.title - Title shown when panel is collapsed and expanded.
425                  * @param {string=}        [options.params.description] - Description shown at the top of the panel.
426                  * @param {number=100}     [options.params.priority] - The sort priority for the panel.
427                  * @param {string=default} [options.params.type] - The type of the panel. See wp.customize.panelConstructor.
428                  * @param {string=}        [options.params.content] - The markup to be used for the panel container. If empty, a JS template is used.
429                  * @param {boolean=true}   [options.params.active] - Whether the panel is active or not.
430                  */
431                 initialize: function ( id, options ) {
432                         var container = this;
433                         container.id = id;
434                         options = options || {};
435
436                         options.params = _.defaults(
437                                 options.params || {},
438                                 container.defaults
439                         );
440
441                         $.extend( container, options );
442                         container.templateSelector = 'customize-' + container.containerType + '-' + container.params.type;
443                         container.container = $( container.params.content );
444                         if ( 0 === container.container.length ) {
445                                 container.container = $( container.getContainer() );
446                         }
447                         container.headContainer = container.container;
448                         container.contentContainer = container.getContent();
449                         container.container = container.container.add( container.contentContainer );
450
451                         container.deferred = {
452                                 embedded: new $.Deferred()
453                         };
454                         container.priority = new api.Value();
455                         container.active = new api.Value();
456                         container.activeArgumentsQueue = [];
457                         container.expanded = new api.Value();
458                         container.expandedArgumentsQueue = [];
459
460                         container.active.bind( function ( active ) {
461                                 var args = container.activeArgumentsQueue.shift();
462                                 args = $.extend( {}, container.defaultActiveArguments, args );
463                                 active = ( active && container.isContextuallyActive() );
464                                 container.onChangeActive( active, args );
465                         });
466                         container.expanded.bind( function ( expanded ) {
467                                 var args = container.expandedArgumentsQueue.shift();
468                                 args = $.extend( {}, container.defaultExpandedArguments, args );
469                                 container.onChangeExpanded( expanded, args );
470                         });
471
472                         container.deferred.embedded.done( function () {
473                                 container.attachEvents();
474                         });
475
476                         api.utils.bubbleChildValueChanges( container, [ 'priority', 'active' ] );
477
478                         container.priority.set( container.params.priority );
479                         container.active.set( container.params.active );
480                         container.expanded.set( false );
481                 },
482
483                 /**
484                  * @since 4.1.0
485                  *
486                  * @abstract
487                  */
488                 ready: function() {},
489
490                 /**
491                  * Get the child models associated with this parent, sorting them by their priority Value.
492                  *
493                  * @since 4.1.0
494                  *
495                  * @param {String} parentType
496                  * @param {String} childType
497                  * @returns {Array}
498                  */
499                 _children: function ( parentType, childType ) {
500                         var parent = this,
501                                 children = [];
502                         api[ childType ].each( function ( child ) {
503                                 if ( child[ parentType ].get() === parent.id ) {
504                                         children.push( child );
505                                 }
506                         } );
507                         children.sort( api.utils.prioritySort );
508                         return children;
509                 },
510
511                 /**
512                  * To override by subclass, to return whether the container has active children.
513                  *
514                  * @since 4.1.0
515                  *
516                  * @abstract
517                  */
518                 isContextuallyActive: function () {
519                         throw new Error( 'Container.isContextuallyActive() must be overridden in a subclass.' );
520                 },
521
522                 /**
523                  * Active state change handler.
524                  *
525                  * Shows the container if it is active, hides it if not.
526                  *
527                  * To override by subclass, update the container's UI to reflect the provided active state.
528                  *
529                  * @since 4.1.0
530                  *
531                  * @param {Boolean} active
532                  * @param {Object}  args
533                  * @param {Object}  args.duration
534                  * @param {Object}  args.completeCallback
535                  */
536                 onChangeActive: function( active, args ) {
537                         var construct = this,
538                                 headContainer = construct.headContainer,
539                                 duration, expandedOtherPanel;
540
541                         if ( args.unchanged ) {
542                                 if ( args.completeCallback ) {
543                                         args.completeCallback();
544                                 }
545                                 return;
546                         }
547
548                         duration = ( 'resolved' === api.previewer.deferred.active.state() ? args.duration : 0 );
549
550                         if ( construct.extended( api.Panel ) ) {
551                                 // If this is a panel is not currently expanded but another panel is expanded, do not animate.
552                                 api.panel.each(function ( panel ) {
553                                         if ( panel !== construct && panel.expanded() ) {
554                                                 expandedOtherPanel = panel;
555                                                 duration = 0;
556                                         }
557                                 });
558
559                                 // Collapse any expanded sections inside of this panel first before deactivating.
560                                 if ( ! active ) {
561                                         _.each( construct.sections(), function( section ) {
562                                                 section.collapse( { duration: 0 } );
563                                         } );
564                                 }
565                         }
566
567                         if ( ! $.contains( document, headContainer ) ) {
568                                 // jQuery.fn.slideUp is not hiding an element if it is not in the DOM
569                                 headContainer.toggle( active );
570                                 if ( args.completeCallback ) {
571                                         args.completeCallback();
572                                 }
573                         } else if ( active ) {
574                                 headContainer.stop( true, true ).slideDown( duration, args.completeCallback );
575                         } else {
576                                 if ( construct.expanded() ) {
577                                         construct.collapse({
578                                                 duration: duration,
579                                                 completeCallback: function() {
580                                                         headContainer.stop( true, true ).slideUp( duration, args.completeCallback );
581                                                 }
582                                         });
583                                 } else {
584                                         headContainer.stop( true, true ).slideUp( duration, args.completeCallback );
585                                 }
586                         }
587                 },
588
589                 /**
590                  * @since 4.1.0
591                  *
592                  * @params {Boolean} active
593                  * @param {Object}   [params]
594                  * @returns {Boolean} false if state already applied
595                  */
596                 _toggleActive: function ( active, params ) {
597                         var self = this;
598                         params = params || {};
599                         if ( ( active && this.active.get() ) || ( ! active && ! this.active.get() ) ) {
600                                 params.unchanged = true;
601                                 self.onChangeActive( self.active.get(), params );
602                                 return false;
603                         } else {
604                                 params.unchanged = false;
605                                 this.activeArgumentsQueue.push( params );
606                                 this.active.set( active );
607                                 return true;
608                         }
609                 },
610
611                 /**
612                  * @param {Object} [params]
613                  * @returns {Boolean} false if already active
614                  */
615                 activate: function ( params ) {
616                         return this._toggleActive( true, params );
617                 },
618
619                 /**
620                  * @param {Object} [params]
621                  * @returns {Boolean} false if already inactive
622                  */
623                 deactivate: function ( params ) {
624                         return this._toggleActive( false, params );
625                 },
626
627                 /**
628                  * To override by subclass, update the container's UI to reflect the provided active state.
629                  * @abstract
630                  */
631                 onChangeExpanded: function () {
632                         throw new Error( 'Must override with subclass.' );
633                 },
634
635                 /**
636                  * Handle the toggle logic for expand/collapse.
637                  *
638                  * @param {Boolean}  expanded - The new state to apply.
639                  * @param {Object}   [params] - Object containing options for expand/collapse.
640                  * @param {Function} [params.completeCallback] - Function to call when expansion/collapse is complete.
641                  * @returns {Boolean} false if state already applied or active state is false
642                  */
643                 _toggleExpanded: function( expanded, params ) {
644                         var instance = this, previousCompleteCallback;
645                         params = params || {};
646                         previousCompleteCallback = params.completeCallback;
647
648                         // Short-circuit expand() if the instance is not active.
649                         if ( expanded && ! instance.active() ) {
650                                 return false;
651                         }
652
653                         api.state( 'paneVisible' ).set( true );
654                         params.completeCallback = function() {
655                                 if ( previousCompleteCallback ) {
656                                         previousCompleteCallback.apply( instance, arguments );
657                                 }
658                                 if ( expanded ) {
659                                         instance.container.trigger( 'expanded' );
660                                 } else {
661                                         instance.container.trigger( 'collapsed' );
662                                 }
663                         };
664                         if ( ( expanded && instance.expanded.get() ) || ( ! expanded && ! instance.expanded.get() ) ) {
665                                 params.unchanged = true;
666                                 instance.onChangeExpanded( instance.expanded.get(), params );
667                                 return false;
668                         } else {
669                                 params.unchanged = false;
670                                 instance.expandedArgumentsQueue.push( params );
671                                 instance.expanded.set( expanded );
672                                 return true;
673                         }
674                 },
675
676                 /**
677                  * @param {Object} [params]
678                  * @returns {Boolean} false if already expanded or if inactive.
679                  */
680                 expand: function ( params ) {
681                         return this._toggleExpanded( true, params );
682                 },
683
684                 /**
685                  * @param {Object} [params]
686                  * @returns {Boolean} false if already collapsed.
687                  */
688                 collapse: function ( params ) {
689                         return this._toggleExpanded( false, params );
690                 },
691
692                 /**
693                  * Animate container state change if transitions are supported by the browser.
694                  *
695                  * @since 4.7.0
696                  * @private
697                  *
698                  * @param {function} completeCallback Function to be called after transition is completed.
699                  * @returns {void}
700                  */
701                 _animateChangeExpanded: function( completeCallback ) {
702                         // Return if CSS transitions are not supported.
703                         if ( ! normalizedTransitionendEventName ) {
704                                 if ( completeCallback ) {
705                                         completeCallback();
706                                 }
707                                 return;
708                         }
709
710                         var construct = this,
711                                 content = construct.contentContainer,
712                                 overlay = content.closest( '.wp-full-overlay' ),
713                                 elements, transitionEndCallback;
714
715                         // Determine set of elements that are affected by the animation.
716                         elements = overlay.add( content );
717                         if ( _.isUndefined( construct.panel ) || '' === construct.panel() ) {
718                                 elements = elements.add( '#customize-info, .customize-pane-parent' );
719                         }
720
721                         // Handle `transitionEnd` event.
722                         transitionEndCallback = function( e ) {
723                                 if ( 2 !== e.eventPhase || ! $( e.target ).is( content ) ) {
724                                         return;
725                                 }
726                                 content.off( normalizedTransitionendEventName, transitionEndCallback );
727                                 elements.removeClass( 'busy' );
728                                 if ( completeCallback ) {
729                                         completeCallback();
730                                 }
731                         };
732                         content.on( normalizedTransitionendEventName, transitionEndCallback );
733                         elements.addClass( 'busy' );
734
735                         // Prevent screen flicker when pane has been scrolled before expanding.
736                         _.defer( function() {
737                                 var container = content.closest( '.wp-full-overlay-sidebar-content' ),
738                                         currentScrollTop = container.scrollTop(),
739                                         previousScrollTop = content.data( 'previous-scrollTop' ) || 0,
740                                         expanded = construct.expanded();
741
742                                 if ( expanded && 0 < currentScrollTop ) {
743                                         content.css( 'top', currentScrollTop + 'px' );
744                                         content.data( 'previous-scrollTop', currentScrollTop );
745                                 } else if ( ! expanded && 0 < currentScrollTop + previousScrollTop ) {
746                                         content.css( 'top', previousScrollTop - currentScrollTop + 'px' );
747                                         container.scrollTop( previousScrollTop );
748                                 }
749                         } );
750                 },
751
752                 /**
753                  * Bring the container into view and then expand this and bring it into view
754                  * @param {Object} [params]
755                  */
756                 focus: focus,
757
758                 /**
759                  * Return the container html, generated from its JS template, if it exists.
760                  *
761                  * @since 4.3.0
762                  */
763                 getContainer: function () {
764                         var template,
765                                 container = this;
766
767                         if ( 0 !== $( '#tmpl-' + container.templateSelector ).length ) {
768                                 template = wp.template( container.templateSelector );
769                         } else {
770                                 template = wp.template( 'customize-' + container.containerType + '-default' );
771                         }
772                         if ( template && container.container ) {
773                                 return $.trim( template( container.params ) );
774                         }
775
776                         return '<li></li>';
777                 },
778
779                 /**
780                  * Find content element which is displayed when the section is expanded.
781                  *
782                  * After a construct is initialized, the return value will be available via the `contentContainer` property.
783                  * By default the element will be related it to the parent container with `aria-owns` and detached.
784                  * Custom panels and sections (such as the `NewMenuSection`) that do not have a sliding pane should
785                  * just return the content element without needing to add the `aria-owns` element or detach it from
786                  * the container. Such non-sliding pane custom sections also need to override the `onChangeExpanded`
787                  * method to handle animating the panel/section into and out of view.
788                  *
789                  * @since 4.7.0
790                  * @access public
791                  *
792                  * @returns {jQuery} Detached content element.
793                  */
794                 getContent: function() {
795                         var construct = this,
796                                 container = construct.container,
797                                 content = container.find( '.accordion-section-content, .control-panel-content' ).first(),
798                                 contentId = 'sub-' + container.attr( 'id' ),
799                                 ownedElements = contentId,
800                                 alreadyOwnedElements = container.attr( 'aria-owns' );
801
802                         if ( alreadyOwnedElements ) {
803                                 ownedElements = ownedElements + ' ' + alreadyOwnedElements;
804                         }
805                         container.attr( 'aria-owns', ownedElements );
806
807                         return content.detach().attr( {
808                                 'id': contentId,
809                                 'class': 'customize-pane-child ' + content.attr( 'class' ) + ' ' + container.attr( 'class' )
810                         } );
811                 }
812         });
813
814         /**
815          * @since 4.1.0
816          *
817          * @class
818          * @augments wp.customize.Class
819          */
820         api.Section = Container.extend({
821                 containerType: 'section',
822                 defaults: {
823                         title: '',
824                         description: '',
825                         priority: 100,
826                         type: 'default',
827                         content: null,
828                         active: true,
829                         instanceNumber: null,
830                         panel: null,
831                         customizeAction: ''
832                 },
833
834                 /**
835                  * @since 4.1.0
836                  *
837                  * @param {string}         id - The ID for the section.
838                  * @param {object}         options - Object containing one property: params.
839                  * @param {object}         options.params - Object containing the following properties.
840                  * @param {string}         options.params.title - Title shown when section is collapsed and expanded.
841                  * @param {string=}        [options.params.description] - Description shown at the top of the section.
842                  * @param {number=100}     [options.params.priority] - The sort priority for the section.
843                  * @param {string=default} [options.params.type] - The type of the section. See wp.customize.sectionConstructor.
844                  * @param {string=}        [options.params.content] - The markup to be used for the section container. If empty, a JS template is used.
845                  * @param {boolean=true}   [options.params.active] - Whether the section is active or not.
846                  * @param {string}         options.params.panel - The ID for the panel this section is associated with.
847                  * @param {string=}        [options.params.customizeAction] - Additional context information shown before the section title when expanded.
848                  */
849                 initialize: function ( id, options ) {
850                         var section = this;
851                         Container.prototype.initialize.call( section, id, options );
852
853                         section.id = id;
854                         section.panel = new api.Value();
855                         section.panel.bind( function ( id ) {
856                                 $( section.headContainer ).toggleClass( 'control-subsection', !! id );
857                         });
858                         section.panel.set( section.params.panel || '' );
859                         api.utils.bubbleChildValueChanges( section, [ 'panel' ] );
860
861                         section.embed();
862                         section.deferred.embedded.done( function () {
863                                 section.ready();
864                         });
865                 },
866
867                 /**
868                  * Embed the container in the DOM when any parent panel is ready.
869                  *
870                  * @since 4.1.0
871                  */
872                 embed: function () {
873                         var inject,
874                                 section = this,
875                                 container = $( '#customize-theme-controls' );
876
877                         // Watch for changes to the panel state
878                         inject = function ( panelId ) {
879                                 var parentContainer;
880                                 if ( panelId ) {
881                                         // The panel has been supplied, so wait until the panel object is registered
882                                         api.panel( panelId, function ( panel ) {
883                                                 // The panel has been registered, wait for it to become ready/initialized
884                                                 panel.deferred.embedded.done( function () {
885                                                         parentContainer = panel.contentContainer;
886                                                         if ( ! section.headContainer.parent().is( parentContainer ) ) {
887                                                                 parentContainer.append( section.headContainer );
888                                                         }
889                                                         if ( ! section.contentContainer.parent().is( section.headContainer ) ) {
890                                                                 container.append( section.contentContainer );
891                                                         }
892                                                         section.deferred.embedded.resolve();
893                                                 });
894                                         } );
895                                 } else {
896                                         // There is no panel, so embed the section in the root of the customizer
897                                         parentContainer = $( '.customize-pane-parent' ); // @todo This should be defined elsewhere, and to be configurable
898                                         if ( ! section.headContainer.parent().is( parentContainer ) ) {
899                                                 parentContainer.append( section.headContainer );
900                                         }
901                                         if ( ! section.contentContainer.parent().is( section.headContainer ) ) {
902                                                 container.append( section.contentContainer );
903                                         }
904                                         section.deferred.embedded.resolve();
905                                 }
906                         };
907                         section.panel.bind( inject );
908                         inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one
909                 },
910
911                 /**
912                  * Add behaviors for the accordion section.
913                  *
914                  * @since 4.1.0
915                  */
916                 attachEvents: function () {
917                         var meta, content, section = this;
918
919                         if ( section.container.hasClass( 'cannot-expand' ) ) {
920                                 return;
921                         }
922
923                         // Expand/Collapse accordion sections on click.
924                         section.container.find( '.accordion-section-title, .customize-section-back' ).on( 'click keydown', function( event ) {
925                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
926                                         return;
927                                 }
928                                 event.preventDefault(); // Keep this AFTER the key filter above
929
930                                 if ( section.expanded() ) {
931                                         section.collapse();
932                                 } else {
933                                         section.expand();
934                                 }
935                         });
936
937                         // This is very similar to what is found for api.Panel.attachEvents().
938                         section.container.find( '.customize-section-title .customize-help-toggle' ).on( 'click', function() {
939
940                                 meta = section.container.find( '.section-meta' );
941                                 if ( meta.hasClass( 'cannot-expand' ) ) {
942                                         return;
943                                 }
944                                 content = meta.find( '.customize-section-description:first' );
945                                 content.toggleClass( 'open' );
946                                 content.slideToggle();
947                                 content.attr( 'aria-expanded', function ( i, attr ) {
948                                         return 'true' === attr ? 'false' : 'true';
949                                 });
950                         });
951                 },
952
953                 /**
954                  * Return whether this section has any active controls.
955                  *
956                  * @since 4.1.0
957                  *
958                  * @returns {Boolean}
959                  */
960                 isContextuallyActive: function () {
961                         var section = this,
962                                 controls = section.controls(),
963                                 activeCount = 0;
964                         _( controls ).each( function ( control ) {
965                                 if ( control.active() ) {
966                                         activeCount += 1;
967                                 }
968                         } );
969                         return ( activeCount !== 0 );
970                 },
971
972                 /**
973                  * Get the controls that are associated with this section, sorted by their priority Value.
974                  *
975                  * @since 4.1.0
976                  *
977                  * @returns {Array}
978                  */
979                 controls: function () {
980                         return this._children( 'section', 'control' );
981                 },
982
983                 /**
984                  * Update UI to reflect expanded state.
985                  *
986                  * @since 4.1.0
987                  *
988                  * @param {Boolean} expanded
989                  * @param {Object}  args
990                  */
991                 onChangeExpanded: function ( expanded, args ) {
992                         var section = this,
993                                 container = section.headContainer.closest( '.wp-full-overlay-sidebar-content' ),
994                                 content = section.contentContainer,
995                                 overlay = section.headContainer.closest( '.wp-full-overlay' ),
996                                 backBtn = content.find( '.customize-section-back' ),
997                                 sectionTitle = section.headContainer.find( '.accordion-section-title' ).first(),
998                                 expand;
999
1000                         if ( expanded && ! content.hasClass( 'open' ) ) {
1001
1002                                 if ( args.unchanged ) {
1003                                         expand = args.completeCallback;
1004                                 } else {
1005                                         expand = $.proxy( function() {
1006                                                 section._animateChangeExpanded( function() {
1007                                                         sectionTitle.attr( 'tabindex', '-1' );
1008                                                         backBtn.attr( 'tabindex', '0' );
1009
1010                                                         backBtn.focus();
1011                                                         content.css( 'top', '' );
1012                                                         container.scrollTop( 0 );
1013
1014                                                         if ( args.completeCallback ) {
1015                                                                 args.completeCallback();
1016                                                         }
1017                                                 } );
1018
1019                                                 content.addClass( 'open' );
1020                                                 overlay.addClass( 'section-open' );
1021                                                 api.state( 'expandedSection' ).set( section );
1022                                         }, this );
1023                                 }
1024
1025                                 if ( ! args.allowMultiple ) {
1026                                         api.section.each( function ( otherSection ) {
1027                                                 if ( otherSection !== section ) {
1028                                                         otherSection.collapse( { duration: args.duration } );
1029                                                 }
1030                                         });
1031                                 }
1032
1033                                 if ( section.panel() ) {
1034                                         api.panel( section.panel() ).expand({
1035                                                 duration: args.duration,
1036                                                 completeCallback: expand
1037                                         });
1038                                 } else {
1039                                         api.panel.each( function( panel ) {
1040                                                 panel.collapse();
1041                                         });
1042                                         expand();
1043                                 }
1044
1045                         } else if ( ! expanded && content.hasClass( 'open' ) ) {
1046                                 section._animateChangeExpanded( function() {
1047                                         backBtn.attr( 'tabindex', '-1' );
1048                                         sectionTitle.attr( 'tabindex', '0' );
1049
1050                                         sectionTitle.focus();
1051                                         content.css( 'top', '' );
1052
1053                                         if ( args.completeCallback ) {
1054                                                 args.completeCallback();
1055                                         }
1056                                 } );
1057
1058                                 content.removeClass( 'open' );
1059                                 overlay.removeClass( 'section-open' );
1060                                 if ( section === api.state( 'expandedSection' ).get() ) {
1061                                         api.state( 'expandedSection' ).set( false );
1062                                 }
1063
1064                         } else {
1065                                 if ( args.completeCallback ) {
1066                                         args.completeCallback();
1067                                 }
1068                         }
1069                 }
1070         });
1071
1072         /**
1073          * wp.customize.ThemesSection
1074          *
1075          * Custom section for themes that functions similarly to a backwards panel,
1076          * and also handles the theme-details view rendering and navigation.
1077          *
1078          * @constructor
1079          * @augments wp.customize.Section
1080          * @augments wp.customize.Container
1081          */
1082         api.ThemesSection = api.Section.extend({
1083                 currentTheme: '',
1084                 overlay: '',
1085                 template: '',
1086                 screenshotQueue: null,
1087                 $window: $( window ),
1088
1089                 /**
1090                  * @since 4.2.0
1091                  */
1092                 initialize: function () {
1093                         this.$customizeSidebar = $( '.wp-full-overlay-sidebar-content:first' );
1094                         return api.Section.prototype.initialize.apply( this, arguments );
1095                 },
1096
1097                 /**
1098                  * @since 4.2.0
1099                  */
1100                 ready: function () {
1101                         var section = this;
1102                         section.overlay = section.container.find( '.theme-overlay' );
1103                         section.template = wp.template( 'customize-themes-details-view' );
1104
1105                         // Bind global keyboard events.
1106                         section.container.on( 'keydown', function( event ) {
1107                                 if ( ! section.overlay.find( '.theme-wrap' ).is( ':visible' ) ) {
1108                                         return;
1109                                 }
1110
1111                                 // Pressing the right arrow key fires a theme:next event
1112                                 if ( 39 === event.keyCode ) {
1113                                         section.nextTheme();
1114                                 }
1115
1116                                 // Pressing the left arrow key fires a theme:previous event
1117                                 if ( 37 === event.keyCode ) {
1118                                         section.previousTheme();
1119                                 }
1120
1121                                 // Pressing the escape key fires a theme:collapse event
1122                                 if ( 27 === event.keyCode ) {
1123                                         section.closeDetails();
1124                                         event.stopPropagation(); // Prevent section from being collapsed.
1125                                 }
1126                         });
1127
1128                         _.bindAll( this, 'renderScreenshots' );
1129                 },
1130
1131                 /**
1132                  * Override Section.isContextuallyActive method.
1133                  *
1134                  * Ignore the active states' of the contained theme controls, and just
1135                  * use the section's own active state instead. This ensures empty search
1136                  * results for themes to cause the section to become inactive.
1137                  *
1138                  * @since 4.2.0
1139                  *
1140                  * @returns {Boolean}
1141                  */
1142                 isContextuallyActive: function () {
1143                         return this.active();
1144                 },
1145
1146                 /**
1147                  * @since 4.2.0
1148                  */
1149                 attachEvents: function () {
1150                         var section = this;
1151
1152                         // Expand/Collapse section/panel.
1153                         section.container.find( '.change-theme, .customize-theme' ).on( 'click keydown', function( event ) {
1154                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1155                                         return;
1156                                 }
1157                                 event.preventDefault(); // Keep this AFTER the key filter above
1158
1159                                 if ( section.expanded() ) {
1160                                         section.collapse();
1161                                 } else {
1162                                         section.expand();
1163                                 }
1164                         });
1165
1166                         // Theme navigation in details view.
1167                         section.container.on( 'click keydown', '.left', function( event ) {
1168                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1169                                         return;
1170                                 }
1171
1172                                 event.preventDefault(); // Keep this AFTER the key filter above
1173
1174                                 section.previousTheme();
1175                         });
1176
1177                         section.container.on( 'click keydown', '.right', function( event ) {
1178                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1179                                         return;
1180                                 }
1181
1182                                 event.preventDefault(); // Keep this AFTER the key filter above
1183
1184                                 section.nextTheme();
1185                         });
1186
1187                         section.container.on( 'click keydown', '.theme-backdrop, .close', function( event ) {
1188                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1189                                         return;
1190                                 }
1191
1192                                 event.preventDefault(); // Keep this AFTER the key filter above
1193
1194                                 section.closeDetails();
1195                         });
1196
1197                         var renderScreenshots = _.throttle( _.bind( section.renderScreenshots, this ), 100 );
1198                         section.container.on( 'input', '#themes-filter', function( event ) {
1199                                 var count,
1200                                         term = event.currentTarget.value.toLowerCase().trim().replace( '-', ' ' ),
1201                                         controls = section.controls();
1202
1203                                 _.each( controls, function( control ) {
1204                                         control.filter( term );
1205                                 });
1206
1207                                 renderScreenshots();
1208
1209                                 // Update theme count.
1210                                 count = section.container.find( 'li.customize-control:visible' ).length;
1211                                 section.container.find( '.theme-count' ).text( count );
1212                         });
1213
1214                         // Pre-load the first 3 theme screenshots.
1215                         api.bind( 'ready', function () {
1216                                 _.each( section.controls().slice( 0, 3 ), function ( control ) {
1217                                         var img, src = control.params.theme.screenshot[0];
1218                                         if ( src ) {
1219                                                 img = new Image();
1220                                                 img.src = src;
1221                                         }
1222                                 });
1223                         });
1224                 },
1225
1226                 /**
1227                  * Update UI to reflect expanded state
1228                  *
1229                  * @since 4.2.0
1230                  *
1231                  * @param {Boolean}  expanded
1232                  * @param {Object}   args
1233                  * @param {Boolean}  args.unchanged
1234                  * @param {Callback} args.completeCallback
1235                  */
1236                 onChangeExpanded: function ( expanded, args ) {
1237
1238                         // Immediately call the complete callback if there were no changes
1239                         if ( args.unchanged ) {
1240                                 if ( args.completeCallback ) {
1241                                         args.completeCallback();
1242                                 }
1243                                 return;
1244                         }
1245
1246                         // Note: there is a second argument 'args' passed
1247                         var panel = this,
1248                                 section = panel.contentContainer,
1249                                 overlay = section.closest( '.wp-full-overlay' ),
1250                                 container = section.closest( '.wp-full-overlay-sidebar-content' ),
1251                                 customizeBtn = section.find( '.customize-theme' ),
1252                                 changeBtn = panel.headContainer.find( '.change-theme' );
1253
1254                         if ( expanded && ! section.hasClass( 'current-panel' ) ) {
1255                                 // Collapse any sibling sections/panels
1256                                 api.section.each( function ( otherSection ) {
1257                                         if ( otherSection !== panel ) {
1258                                                 otherSection.collapse( { duration: args.duration } );
1259                                         }
1260                                 });
1261                                 api.panel.each( function ( otherPanel ) {
1262                                         otherPanel.collapse( { duration: 0 } );
1263                                 });
1264
1265                                 panel._animateChangeExpanded( function() {
1266                                         changeBtn.attr( 'tabindex', '-1' );
1267                                         customizeBtn.attr( 'tabindex', '0' );
1268
1269                                         customizeBtn.focus();
1270                                         section.css( 'top', '' );
1271                                         container.scrollTop( 0 );
1272
1273                                         if ( args.completeCallback ) {
1274                                                 args.completeCallback();
1275                                         }
1276                                 } );
1277
1278                                 overlay.addClass( 'in-themes-panel' );
1279                                 section.addClass( 'current-panel' );
1280                                 _.delay( panel.renderScreenshots, 10 ); // Wait for the controls
1281                                 panel.$customizeSidebar.on( 'scroll.customize-themes-section', _.throttle( panel.renderScreenshots, 300 ) );
1282
1283                         } else if ( ! expanded && section.hasClass( 'current-panel' ) ) {
1284                                 panel._animateChangeExpanded( function() {
1285                                         changeBtn.attr( 'tabindex', '0' );
1286                                         customizeBtn.attr( 'tabindex', '-1' );
1287
1288                                         changeBtn.focus();
1289                                         section.css( 'top', '' );
1290
1291                                         if ( args.completeCallback ) {
1292                                                 args.completeCallback();
1293                                         }
1294                                 } );
1295
1296                                 overlay.removeClass( 'in-themes-panel' );
1297                                 section.removeClass( 'current-panel' );
1298                                 panel.$customizeSidebar.off( 'scroll.customize-themes-section' );
1299                         }
1300                 },
1301
1302                 /**
1303                  * Render control's screenshot if the control comes into view.
1304                  *
1305                  * @since 4.2.0
1306                  */
1307                 renderScreenshots: function( ) {
1308                         var section = this;
1309
1310                         // Fill queue initially.
1311                         if ( section.screenshotQueue === null ) {
1312                                 section.screenshotQueue = section.controls();
1313                         }
1314
1315                         // Are all screenshots rendered?
1316                         if ( ! section.screenshotQueue.length ) {
1317                                 return;
1318                         }
1319
1320                         section.screenshotQueue = _.filter( section.screenshotQueue, function( control ) {
1321                                 var $imageWrapper = control.container.find( '.theme-screenshot' ),
1322                                         $image = $imageWrapper.find( 'img' );
1323
1324                                 if ( ! $image.length ) {
1325                                         return false;
1326                                 }
1327
1328                                 if ( $image.is( ':hidden' ) ) {
1329                                         return true;
1330                                 }
1331
1332                                 // Based on unveil.js.
1333                                 var wt = section.$window.scrollTop(),
1334                                         wb = wt + section.$window.height(),
1335                                         et = $image.offset().top,
1336                                         ih = $imageWrapper.height(),
1337                                         eb = et + ih,
1338                                         threshold = ih * 3,
1339                                         inView = eb >= wt - threshold && et <= wb + threshold;
1340
1341                                 if ( inView ) {
1342                                         control.container.trigger( 'render-screenshot' );
1343                                 }
1344
1345                                 // If the image is in view return false so it's cleared from the queue.
1346                                 return ! inView;
1347                         } );
1348                 },
1349
1350                 /**
1351                  * Advance the modal to the next theme.
1352                  *
1353                  * @since 4.2.0
1354                  */
1355                 nextTheme: function () {
1356                         var section = this;
1357                         if ( section.getNextTheme() ) {
1358                                 section.showDetails( section.getNextTheme(), function() {
1359                                         section.overlay.find( '.right' ).focus();
1360                                 } );
1361                         }
1362                 },
1363
1364                 /**
1365                  * Get the next theme model.
1366                  *
1367                  * @since 4.2.0
1368                  */
1369                 getNextTheme: function () {
1370                         var control, next;
1371                         control = api.control( 'theme_' + this.currentTheme );
1372                         next = control.container.next( 'li.customize-control-theme' );
1373                         if ( ! next.length ) {
1374                                 return false;
1375                         }
1376                         next = next[0].id.replace( 'customize-control-', '' );
1377                         control = api.control( next );
1378
1379                         return control.params.theme;
1380                 },
1381
1382                 /**
1383                  * Advance the modal to the previous theme.
1384                  *
1385                  * @since 4.2.0
1386                  */
1387                 previousTheme: function () {
1388                         var section = this;
1389                         if ( section.getPreviousTheme() ) {
1390                                 section.showDetails( section.getPreviousTheme(), function() {
1391                                         section.overlay.find( '.left' ).focus();
1392                                 } );
1393                         }
1394                 },
1395
1396                 /**
1397                  * Get the previous theme model.
1398                  *
1399                  * @since 4.2.0
1400                  */
1401                 getPreviousTheme: function () {
1402                         var control, previous;
1403                         control = api.control( 'theme_' + this.currentTheme );
1404                         previous = control.container.prev( 'li.customize-control-theme' );
1405                         if ( ! previous.length ) {
1406                                 return false;
1407                         }
1408                         previous = previous[0].id.replace( 'customize-control-', '' );
1409                         control = api.control( previous );
1410
1411                         return control.params.theme;
1412                 },
1413
1414                 /**
1415                  * Disable buttons when we're viewing the first or last theme.
1416                  *
1417                  * @since 4.2.0
1418                  */
1419                 updateLimits: function () {
1420                         if ( ! this.getNextTheme() ) {
1421                                 this.overlay.find( '.right' ).addClass( 'disabled' );
1422                         }
1423                         if ( ! this.getPreviousTheme() ) {
1424                                 this.overlay.find( '.left' ).addClass( 'disabled' );
1425                         }
1426                 },
1427
1428                 /**
1429                  * Load theme preview.
1430                  *
1431                  * @since 4.7.0
1432                  * @access public
1433                  *
1434                  * @param {string} themeId Theme ID.
1435                  * @returns {jQuery.promise} Promise.
1436                  */
1437                 loadThemePreview: function( themeId ) {
1438                         var deferred = $.Deferred(), onceProcessingComplete, overlay, urlParser;
1439
1440                         urlParser = document.createElement( 'a' );
1441                         urlParser.href = location.href;
1442                         urlParser.search = $.param( _.extend(
1443                                 api.utils.parseQueryString( urlParser.search.substr( 1 ) ),
1444                                 {
1445                                         theme: themeId,
1446                                         changeset_uuid: api.settings.changeset.uuid
1447                                 }
1448                         ) );
1449
1450                         overlay = $( '.wp-full-overlay' );
1451                         overlay.addClass( 'customize-loading' );
1452
1453                         onceProcessingComplete = function() {
1454                                 var request;
1455                                 if ( api.state( 'processing' ).get() > 0 ) {
1456                                         return;
1457                                 }
1458
1459                                 api.state( 'processing' ).unbind( onceProcessingComplete );
1460
1461                                 request = api.requestChangesetUpdate();
1462                                 request.done( function() {
1463                                         $( window ).off( 'beforeunload.customize-confirm' );
1464                                         top.location.href = urlParser.href;
1465                                         deferred.resolve();
1466                                 } );
1467                                 request.fail( function() {
1468                                         overlay.removeClass( 'customize-loading' );
1469                                         deferred.reject();
1470                                 } );
1471                         };
1472
1473                         if ( 0 === api.state( 'processing' ).get() ) {
1474                                 onceProcessingComplete();
1475                         } else {
1476                                 api.state( 'processing' ).bind( onceProcessingComplete );
1477                         }
1478
1479                         return deferred.promise();
1480                 },
1481
1482                 /**
1483                  * Render & show the theme details for a given theme model.
1484                  *
1485                  * @since 4.2.0
1486                  *
1487                  * @param {Object}   theme
1488                  */
1489                 showDetails: function ( theme, callback ) {
1490                         var section = this, link;
1491                         callback = callback || function(){};
1492                         section.currentTheme = theme.id;
1493                         section.overlay.html( section.template( theme ) )
1494                                 .fadeIn( 'fast' )
1495                                 .focus();
1496                         $( 'body' ).addClass( 'modal-open' );
1497                         section.containFocus( section.overlay );
1498                         section.updateLimits();
1499
1500                         link = section.overlay.find( '.inactive-theme > a' );
1501
1502                         link.on( 'click', function( event ) {
1503                                 event.preventDefault();
1504
1505                                 // Short-circuit if request is currently being made.
1506                                 if ( link.hasClass( 'disabled' ) ) {
1507                                         return;
1508                                 }
1509                                 link.addClass( 'disabled' );
1510
1511                                 section.loadThemePreview( theme.id ).fail( function() {
1512                                         link.removeClass( 'disabled' );
1513                                 } );
1514                         } );
1515                         callback();
1516                 },
1517
1518                 /**
1519                  * Close the theme details modal.
1520                  *
1521                  * @since 4.2.0
1522                  */
1523                 closeDetails: function () {
1524                         $( 'body' ).removeClass( 'modal-open' );
1525                         this.overlay.fadeOut( 'fast' );
1526                         api.control( 'theme_' + this.currentTheme ).focus();
1527                 },
1528
1529                 /**
1530                  * Keep tab focus within the theme details modal.
1531                  *
1532                  * @since 4.2.0
1533                  */
1534                 containFocus: function( el ) {
1535                         var tabbables;
1536
1537                         el.on( 'keydown', function( event ) {
1538
1539                                 // Return if it's not the tab key
1540                                 // When navigating with prev/next focus is already handled
1541                                 if ( 9 !== event.keyCode ) {
1542                                         return;
1543                                 }
1544
1545                                 // uses jQuery UI to get the tabbable elements
1546                                 tabbables = $( ':tabbable', el );
1547
1548                                 // Keep focus within the overlay
1549                                 if ( tabbables.last()[0] === event.target && ! event.shiftKey ) {
1550                                         tabbables.first().focus();
1551                                         return false;
1552                                 } else if ( tabbables.first()[0] === event.target && event.shiftKey ) {
1553                                         tabbables.last().focus();
1554                                         return false;
1555                                 }
1556                         });
1557                 }
1558         });
1559
1560         /**
1561          * @since 4.1.0
1562          *
1563          * @class
1564          * @augments wp.customize.Class
1565          */
1566         api.Panel = Container.extend({
1567                 containerType: 'panel',
1568
1569                 /**
1570                  * @since 4.1.0
1571                  *
1572                  * @param {string}         id - The ID for the panel.
1573                  * @param {object}         options - Object containing one property: params.
1574                  * @param {object}         options.params - Object containing the following properties.
1575                  * @param {string}         options.params.title - Title shown when panel is collapsed and expanded.
1576                  * @param {string=}        [options.params.description] - Description shown at the top of the panel.
1577                  * @param {number=100}     [options.params.priority] - The sort priority for the panel.
1578                  * @param {string=default} [options.params.type] - The type of the panel. See wp.customize.panelConstructor.
1579                  * @param {string=}        [options.params.content] - The markup to be used for the panel container. If empty, a JS template is used.
1580                  * @param {boolean=true}   [options.params.active] - Whether the panel is active or not.
1581                  */
1582                 initialize: function ( id, options ) {
1583                         var panel = this;
1584                         Container.prototype.initialize.call( panel, id, options );
1585                         panel.embed();
1586                         panel.deferred.embedded.done( function () {
1587                                 panel.ready();
1588                         });
1589                 },
1590
1591                 /**
1592                  * Embed the container in the DOM when any parent panel is ready.
1593                  *
1594                  * @since 4.1.0
1595                  */
1596                 embed: function () {
1597                         var panel = this,
1598                                 container = $( '#customize-theme-controls' ),
1599                                 parentContainer = $( '.customize-pane-parent' ); // @todo This should be defined elsewhere, and to be configurable
1600
1601                         if ( ! panel.headContainer.parent().is( parentContainer ) ) {
1602                                 parentContainer.append( panel.headContainer );
1603                         }
1604                         if ( ! panel.contentContainer.parent().is( panel.headContainer ) ) {
1605                                 container.append( panel.contentContainer );
1606                                 panel.renderContent();
1607                         }
1608
1609                         panel.deferred.embedded.resolve();
1610                 },
1611
1612                 /**
1613                  * @since 4.1.0
1614                  */
1615                 attachEvents: function () {
1616                         var meta, panel = this;
1617
1618                         // Expand/Collapse accordion sections on click.
1619                         panel.headContainer.find( '.accordion-section-title' ).on( 'click keydown', function( event ) {
1620                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1621                                         return;
1622                                 }
1623                                 event.preventDefault(); // Keep this AFTER the key filter above
1624
1625                                 if ( ! panel.expanded() ) {
1626                                         panel.expand();
1627                                 }
1628                         });
1629
1630                         // Close panel.
1631                         panel.container.find( '.customize-panel-back' ).on( 'click keydown', function( event ) {
1632                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1633                                         return;
1634                                 }
1635                                 event.preventDefault(); // Keep this AFTER the key filter above
1636
1637                                 if ( panel.expanded() ) {
1638                                         panel.collapse();
1639                                 }
1640                         });
1641
1642                         meta = panel.container.find( '.panel-meta:first' );
1643
1644                         meta.find( '> .accordion-section-title .customize-help-toggle' ).on( 'click keydown', function( event ) {
1645                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1646                                         return;
1647                                 }
1648                                 event.preventDefault(); // Keep this AFTER the key filter above
1649
1650                                 if ( meta.hasClass( 'cannot-expand' ) ) {
1651                                         return;
1652                                 }
1653
1654                                 var content = meta.find( '.customize-panel-description:first' );
1655                                 if ( meta.hasClass( 'open' ) ) {
1656                                         meta.toggleClass( 'open' );
1657                                         content.slideUp( panel.defaultExpandedArguments.duration );
1658                                         $( this ).attr( 'aria-expanded', false );
1659                                 } else {
1660                                         content.slideDown( panel.defaultExpandedArguments.duration );
1661                                         meta.toggleClass( 'open' );
1662                                         $( this ).attr( 'aria-expanded', true );
1663                                 }
1664                         });
1665
1666                 },
1667
1668                 /**
1669                  * Get the sections that are associated with this panel, sorted by their priority Value.
1670                  *
1671                  * @since 4.1.0
1672                  *
1673                  * @returns {Array}
1674                  */
1675                 sections: function () {
1676                         return this._children( 'panel', 'section' );
1677                 },
1678
1679                 /**
1680                  * Return whether this panel has any active sections.
1681                  *
1682                  * @since 4.1.0
1683                  *
1684                  * @returns {boolean}
1685                  */
1686                 isContextuallyActive: function () {
1687                         var panel = this,
1688                                 sections = panel.sections(),
1689                                 activeCount = 0;
1690                         _( sections ).each( function ( section ) {
1691                                 if ( section.active() && section.isContextuallyActive() ) {
1692                                         activeCount += 1;
1693                                 }
1694                         } );
1695                         return ( activeCount !== 0 );
1696                 },
1697
1698                 /**
1699                  * Update UI to reflect expanded state
1700                  *
1701                  * @since 4.1.0
1702                  *
1703                  * @param {Boolean}  expanded
1704                  * @param {Object}   args
1705                  * @param {Boolean}  args.unchanged
1706                  * @param {Function} args.completeCallback
1707                  */
1708                 onChangeExpanded: function ( expanded, args ) {
1709
1710                         // Immediately call the complete callback if there were no changes
1711                         if ( args.unchanged ) {
1712                                 if ( args.completeCallback ) {
1713                                         args.completeCallback();
1714                                 }
1715                                 return;
1716                         }
1717
1718                         // Note: there is a second argument 'args' passed
1719                         var panel = this,
1720                                 accordionSection = panel.contentContainer,
1721                                 overlay = accordionSection.closest( '.wp-full-overlay' ),
1722                                 container = accordionSection.closest( '.wp-full-overlay-sidebar-content' ),
1723                                 topPanel = panel.headContainer.find( '.accordion-section-title' ),
1724                                 backBtn = accordionSection.find( '.customize-panel-back' );
1725
1726                         if ( expanded && ! accordionSection.hasClass( 'current-panel' ) ) {
1727                                 // Collapse any sibling sections/panels
1728                                 api.section.each( function ( section ) {
1729                                         if ( panel.id !== section.panel() ) {
1730                                                 section.collapse( { duration: 0 } );
1731                                         }
1732                                 });
1733                                 api.panel.each( function ( otherPanel ) {
1734                                         if ( panel !== otherPanel ) {
1735                                                 otherPanel.collapse( { duration: 0 } );
1736                                         }
1737                                 });
1738
1739                                 panel._animateChangeExpanded( function() {
1740                                         topPanel.attr( 'tabindex', '-1' );
1741                                         backBtn.attr( 'tabindex', '0' );
1742
1743                                         backBtn.focus();
1744                                         accordionSection.css( 'top', '' );
1745                                         container.scrollTop( 0 );
1746
1747                                         if ( args.completeCallback ) {
1748                                                 args.completeCallback();
1749                                         }
1750                                 } );
1751
1752                                 overlay.addClass( 'in-sub-panel' );
1753                                 accordionSection.addClass( 'current-panel' );
1754                                 api.state( 'expandedPanel' ).set( panel );
1755
1756                         } else if ( ! expanded && accordionSection.hasClass( 'current-panel' ) ) {
1757                                 panel._animateChangeExpanded( function() {
1758                                         topPanel.attr( 'tabindex', '0' );
1759                                         backBtn.attr( 'tabindex', '-1' );
1760
1761                                         topPanel.focus();
1762                                         accordionSection.css( 'top', '' );
1763
1764                                         if ( args.completeCallback ) {
1765                                                 args.completeCallback();
1766                                         }
1767                                 } );
1768
1769                                 overlay.removeClass( 'in-sub-panel' );
1770                                 accordionSection.removeClass( 'current-panel' );
1771                                 if ( panel === api.state( 'expandedPanel' ).get() ) {
1772                                         api.state( 'expandedPanel' ).set( false );
1773                                 }
1774                         }
1775                 },
1776
1777                 /**
1778                  * Render the panel from its JS template, if it exists.
1779                  *
1780                  * The panel's container must already exist in the DOM.
1781                  *
1782                  * @since 4.3.0
1783                  */
1784                 renderContent: function () {
1785                         var template,
1786                                 panel = this;
1787
1788                         // Add the content to the container.
1789                         if ( 0 !== $( '#tmpl-' + panel.templateSelector + '-content' ).length ) {
1790                                 template = wp.template( panel.templateSelector + '-content' );
1791                         } else {
1792                                 template = wp.template( 'customize-panel-default-content' );
1793                         }
1794                         if ( template && panel.headContainer ) {
1795                                 panel.contentContainer.html( template( panel.params ) );
1796                         }
1797                 }
1798         });
1799
1800         /**
1801          * A Customizer Control.
1802          *
1803          * A control provides a UI element that allows a user to modify a Customizer Setting.
1804          *
1805          * @see PHP class WP_Customize_Control.
1806          *
1807          * @class
1808          * @augments wp.customize.Class
1809          *
1810          * @param {string} id                              Unique identifier for the control instance.
1811          * @param {object} options                         Options hash for the control instance.
1812          * @param {object} options.params
1813          * @param {object} options.params.type             Type of control (e.g. text, radio, dropdown-pages, etc.)
1814          * @param {string} options.params.content          The HTML content for the control.
1815          * @param {string} options.params.priority         Order of priority to show the control within the section.
1816          * @param {string} options.params.active
1817          * @param {string} options.params.section          The ID of the section the control belongs to.
1818          * @param {string} options.params.settings.default The ID of the setting the control relates to.
1819          * @param {string} options.params.settings.data
1820          * @param {string} options.params.label
1821          * @param {string} options.params.description
1822          * @param {string} options.params.instanceNumber Order in which this instance was created in relation to other instances.
1823          */
1824         api.Control = api.Class.extend({
1825                 defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
1826
1827                 initialize: function( id, options ) {
1828                         var control = this,
1829                                 nodes, radios, settings;
1830
1831                         control.params = {};
1832                         $.extend( control, options || {} );
1833                         control.id = id;
1834                         control.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' );
1835                         control.templateSelector = 'customize-control-' + control.params.type + '-content';
1836                         control.container = control.params.content ? $( control.params.content ) : $( control.selector );
1837
1838                         control.deferred = {
1839                                 embedded: new $.Deferred()
1840                         };
1841                         control.section = new api.Value();
1842                         control.priority = new api.Value();
1843                         control.active = new api.Value();
1844                         control.activeArgumentsQueue = [];
1845                         control.notifications = new api.Values({ defaultConstructor: api.Notification });
1846
1847                         control.elements = [];
1848
1849                         nodes  = control.container.find('[data-customize-setting-link]');
1850                         radios = {};
1851
1852                         nodes.each( function() {
1853                                 var node = $( this ),
1854                                         name;
1855
1856                                 if ( node.is( ':radio' ) ) {
1857                                         name = node.prop( 'name' );
1858                                         if ( radios[ name ] ) {
1859                                                 return;
1860                                         }
1861
1862                                         radios[ name ] = true;
1863                                         node = nodes.filter( '[name="' + name + '"]' );
1864                                 }
1865
1866                                 api( node.data( 'customizeSettingLink' ), function( setting ) {
1867                                         var element = new api.Element( node );
1868                                         control.elements.push( element );
1869                                         element.sync( setting );
1870                                         element.set( setting() );
1871                                 });
1872                         });
1873
1874                         control.active.bind( function ( active ) {
1875                                 var args = control.activeArgumentsQueue.shift();
1876                                 args = $.extend( {}, control.defaultActiveArguments, args );
1877                                 control.onChangeActive( active, args );
1878                         } );
1879
1880                         control.section.set( control.params.section );
1881                         control.priority.set( isNaN( control.params.priority ) ? 10 : control.params.priority );
1882                         control.active.set( control.params.active );
1883
1884                         api.utils.bubbleChildValueChanges( control, [ 'section', 'priority', 'active' ] );
1885
1886                         /*
1887                          * After all settings related to the control are available,
1888                          * make them available on the control and embed the control into the page.
1889                          */
1890                         settings = $.map( control.params.settings, function( value ) {
1891                                 return value;
1892                         });
1893
1894                         if ( 0 === settings.length ) {
1895                                 control.setting = null;
1896                                 control.settings = {};
1897                                 control.embed();
1898                         } else {
1899                                 api.apply( api, settings.concat( function() {
1900                                         var key;
1901
1902                                         control.settings = {};
1903                                         for ( key in control.params.settings ) {
1904                                                 control.settings[ key ] = api( control.params.settings[ key ] );
1905                                         }
1906
1907                                         control.setting = control.settings['default'] || null;
1908
1909                                         // Add setting notifications to the control notification.
1910                                         _.each( control.settings, function( setting ) {
1911                                                 setting.notifications.bind( 'add', function( settingNotification ) {
1912                                                         var controlNotification, code, params;
1913                                                         code = setting.id + ':' + settingNotification.code;
1914                                                         params = _.extend(
1915                                                                 {},
1916                                                                 settingNotification,
1917                                                                 {
1918                                                                         setting: setting.id
1919                                                                 }
1920                                                         );
1921                                                         controlNotification = new api.Notification( code, params );
1922                                                         control.notifications.add( controlNotification.code, controlNotification );
1923                                                 } );
1924                                                 setting.notifications.bind( 'remove', function( settingNotification ) {
1925                                                         control.notifications.remove( setting.id + ':' + settingNotification.code );
1926                                                 } );
1927                                         } );
1928
1929                                         control.embed();
1930                                 }) );
1931                         }
1932
1933                         // After the control is embedded on the page, invoke the "ready" method.
1934                         control.deferred.embedded.done( function () {
1935                                 /*
1936                                  * Note that this debounced/deferred rendering is needed for two reasons:
1937                                  * 1) The 'remove' event is triggered just _before_ the notification is actually removed.
1938                                  * 2) Improve performance when adding/removing multiple notifications at a time.
1939                                  */
1940                                 var debouncedRenderNotifications = _.debounce( function renderNotifications() {
1941                                         control.renderNotifications();
1942                                 } );
1943                                 control.notifications.bind( 'add', function( notification ) {
1944                                         wp.a11y.speak( notification.message, 'assertive' );
1945                                         debouncedRenderNotifications();
1946                                 } );
1947                                 control.notifications.bind( 'remove', debouncedRenderNotifications );
1948                                 control.renderNotifications();
1949
1950                                 control.ready();
1951                         });
1952                 },
1953
1954                 /**
1955                  * Embed the control into the page.
1956                  */
1957                 embed: function () {
1958                         var control = this,
1959                                 inject;
1960
1961                         // Watch for changes to the section state
1962                         inject = function ( sectionId ) {
1963                                 var parentContainer;
1964                                 if ( ! sectionId ) { // @todo allow a control to be embedded without a section, for instance a control embedded in the front end.
1965                                         return;
1966                                 }
1967                                 // Wait for the section to be registered
1968                                 api.section( sectionId, function ( section ) {
1969                                         // Wait for the section to be ready/initialized
1970                                         section.deferred.embedded.done( function () {
1971                                                 parentContainer = ( section.contentContainer.is( 'ul' ) ) ? section.contentContainer : section.contentContainer.find( 'ul:first' );
1972                                                 if ( ! control.container.parent().is( parentContainer ) ) {
1973                                                         parentContainer.append( control.container );
1974                                                         control.renderContent();
1975                                                 }
1976                                                 control.deferred.embedded.resolve();
1977                                         });
1978                                 });
1979                         };
1980                         control.section.bind( inject );
1981                         inject( control.section.get() );
1982                 },
1983
1984                 /**
1985                  * Triggered when the control's markup has been injected into the DOM.
1986                  *
1987                  * @returns {void}
1988                  */
1989                 ready: function() {
1990                         var control = this, newItem;
1991                         if ( 'dropdown-pages' === control.params.type && control.params.allow_addition ) {
1992                                 newItem = control.container.find( '.new-content-item' );
1993                                 newItem.hide(); // Hide in JS to preserve flex display when showing.
1994                                 control.container.on( 'click', '.add-new-toggle', function( e ) {
1995                                         $( e.currentTarget ).slideUp( 180 );
1996                                         newItem.slideDown( 180 );
1997                                         newItem.find( '.create-item-input' ).focus();
1998                                 });
1999                                 control.container.on( 'click', '.add-content', function() {
2000                                         control.addNewPage();
2001                                 });
2002                                 control.container.on( 'keyup', '.create-item-input', function( e ) {
2003                                         if ( 13 === e.which ) { // Enter
2004                                                 control.addNewPage();
2005                                         }
2006                                 });
2007                         }
2008                 },
2009
2010                 /**
2011                  * Get the element inside of a control's container that contains the validation error message.
2012                  *
2013                  * Control subclasses may override this to return the proper container to render notifications into.
2014                  * Injects the notification container for existing controls that lack the necessary container,
2015                  * including special handling for nav menu items and widgets.
2016                  *
2017                  * @since 4.6.0
2018                  * @returns {jQuery} Setting validation message element.
2019                  * @this {wp.customize.Control}
2020                  */
2021                 getNotificationsContainerElement: function() {
2022                         var control = this, controlTitle, notificationsContainer;
2023
2024                         notificationsContainer = control.container.find( '.customize-control-notifications-container:first' );
2025                         if ( notificationsContainer.length ) {
2026                                 return notificationsContainer;
2027                         }
2028
2029                         notificationsContainer = $( '<div class="customize-control-notifications-container"></div>' );
2030
2031                         if ( control.container.hasClass( 'customize-control-nav_menu_item' ) ) {
2032                                 control.container.find( '.menu-item-settings:first' ).prepend( notificationsContainer );
2033                         } else if ( control.container.hasClass( 'customize-control-widget_form' ) ) {
2034                                 control.container.find( '.widget-inside:first' ).prepend( notificationsContainer );
2035                         } else {
2036                                 controlTitle = control.container.find( '.customize-control-title' );
2037                                 if ( controlTitle.length ) {
2038                                         controlTitle.after( notificationsContainer );
2039                                 } else {
2040                                         control.container.prepend( notificationsContainer );
2041                                 }
2042                         }
2043                         return notificationsContainer;
2044                 },
2045
2046                 /**
2047                  * Render notifications.
2048                  *
2049                  * Renders the `control.notifications` into the control's container.
2050                  * Control subclasses may override this method to do their own handling
2051                  * of rendering notifications.
2052                  *
2053                  * @since 4.6.0
2054                  * @this {wp.customize.Control}
2055                  */
2056                 renderNotifications: function() {
2057                         var control = this, container, notifications, hasError = false;
2058                         container = control.getNotificationsContainerElement();
2059                         if ( ! container || ! container.length ) {
2060                                 return;
2061                         }
2062                         notifications = [];
2063                         control.notifications.each( function( notification ) {
2064                                 notifications.push( notification );
2065                                 if ( 'error' === notification.type ) {
2066                                         hasError = true;
2067                                 }
2068                         } );
2069
2070                         if ( 0 === notifications.length ) {
2071                                 container.stop().slideUp( 'fast' );
2072                         } else {
2073                                 container.stop().slideDown( 'fast', null, function() {
2074                                         $( this ).css( 'height', 'auto' );
2075                                 } );
2076                         }
2077
2078                         if ( ! control.notificationsTemplate ) {
2079                                 control.notificationsTemplate = wp.template( 'customize-control-notifications' );
2080                         }
2081
2082                         control.container.toggleClass( 'has-notifications', 0 !== notifications.length );
2083                         control.container.toggleClass( 'has-error', hasError );
2084                         container.empty().append( $.trim(
2085                                 control.notificationsTemplate( { notifications: notifications, altNotice: Boolean( control.altNotice ) } )
2086                         ) );
2087                 },
2088
2089                 /**
2090                  * Normal controls do not expand, so just expand its parent
2091                  *
2092                  * @param {Object} [params]
2093                  */
2094                 expand: function ( params ) {
2095                         api.section( this.section() ).expand( params );
2096                 },
2097
2098                 /**
2099                  * Bring the containing section and panel into view and then
2100                  * this control into view, focusing on the first input.
2101                  */
2102                 focus: focus,
2103
2104                 /**
2105                  * Update UI in response to a change in the control's active state.
2106                  * This does not change the active state, it merely handles the behavior
2107                  * for when it does change.
2108                  *
2109                  * @since 4.1.0
2110                  *
2111                  * @param {Boolean}  active
2112                  * @param {Object}   args
2113                  * @param {Number}   args.duration
2114                  * @param {Callback} args.completeCallback
2115                  */
2116                 onChangeActive: function ( active, args ) {
2117                         if ( args.unchanged ) {
2118                                 if ( args.completeCallback ) {
2119                                         args.completeCallback();
2120                                 }
2121                                 return;
2122                         }
2123
2124                         if ( ! $.contains( document, this.container[0] ) ) {
2125                                 // jQuery.fn.slideUp is not hiding an element if it is not in the DOM
2126                                 this.container.toggle( active );
2127                                 if ( args.completeCallback ) {
2128                                         args.completeCallback();
2129                                 }
2130                         } else if ( active ) {
2131                                 this.container.slideDown( args.duration, args.completeCallback );
2132                         } else {
2133                                 this.container.slideUp( args.duration, args.completeCallback );
2134                         }
2135                 },
2136
2137                 /**
2138                  * @deprecated 4.1.0 Use this.onChangeActive() instead.
2139                  */
2140                 toggle: function ( active ) {
2141                         return this.onChangeActive( active, this.defaultActiveArguments );
2142                 },
2143
2144                 /**
2145                  * Shorthand way to enable the active state.
2146                  *
2147                  * @since 4.1.0
2148                  *
2149                  * @param {Object} [params]
2150                  * @returns {Boolean} false if already active
2151                  */
2152                 activate: Container.prototype.activate,
2153
2154                 /**
2155                  * Shorthand way to disable the active state.
2156                  *
2157                  * @since 4.1.0
2158                  *
2159                  * @param {Object} [params]
2160                  * @returns {Boolean} false if already inactive
2161                  */
2162                 deactivate: Container.prototype.deactivate,
2163
2164                 /**
2165                  * Re-use _toggleActive from Container class.
2166                  *
2167                  * @access private
2168                  */
2169                 _toggleActive: Container.prototype._toggleActive,
2170
2171                 dropdownInit: function() {
2172                         var control      = this,
2173                                 statuses     = this.container.find('.dropdown-status'),
2174                                 params       = this.params,
2175                                 toggleFreeze = false,
2176                                 update       = function( to ) {
2177                                         if ( typeof to === 'string' && params.statuses && params.statuses[ to ] )
2178                                                 statuses.html( params.statuses[ to ] ).show();
2179                                         else
2180                                                 statuses.hide();
2181                                 };
2182
2183                         // Support the .dropdown class to open/close complex elements
2184                         this.container.on( 'click keydown', '.dropdown', function( event ) {
2185                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2186                                         return;
2187                                 }
2188
2189                                 event.preventDefault();
2190
2191                                 if (!toggleFreeze)
2192                                         control.container.toggleClass('open');
2193
2194                                 if ( control.container.hasClass('open') )
2195                                         control.container.parent().parent().find('li.library-selected').focus();
2196
2197                                 // Don't want to fire focus and click at same time
2198                                 toggleFreeze = true;
2199                                 setTimeout(function () {
2200                                         toggleFreeze = false;
2201                                 }, 400);
2202                         });
2203
2204                         this.setting.bind( update );
2205                         update( this.setting() );
2206                 },
2207
2208                 /**
2209                  * Render the control from its JS template, if it exists.
2210                  *
2211                  * The control's container must already exist in the DOM.
2212                  *
2213                  * @since 4.1.0
2214                  */
2215                 renderContent: function () {
2216                         var template,
2217                                 control = this;
2218
2219                         // Replace the container element's content with the control.
2220                         if ( 0 !== $( '#tmpl-' + control.templateSelector ).length ) {
2221                                 template = wp.template( control.templateSelector );
2222                                 if ( template && control.container ) {
2223                                         control.container.html( template( control.params ) );
2224                                 }
2225                         }
2226                 },
2227
2228                 /**
2229                  * Add a new page to a dropdown-pages control reusing menus code for this.
2230                  *
2231                  * @since 4.7.0
2232                  * @access private
2233                  * @returns {void}
2234                  */
2235                 addNewPage: function () {
2236                         var control = this, promise, toggle, container, input, title, select;
2237
2238                         if ( 'dropdown-pages' !== control.params.type || ! control.params.allow_addition || ! api.Menus ) {
2239                                 return;
2240                         }
2241
2242                         toggle = control.container.find( '.add-new-toggle' );
2243                         container = control.container.find( '.new-content-item' );
2244                         input = control.container.find( '.create-item-input' );
2245                         title = input.val();
2246                         select = control.container.find( 'select' );
2247
2248                         if ( ! title ) {
2249                                 input.addClass( 'invalid' );
2250                                 return;
2251                         }
2252
2253                         input.removeClass( 'invalid' );
2254                         input.attr( 'disabled', 'disabled' );
2255
2256                         // The menus functions add the page, publish when appropriate, and also add the new page to the dropdown-pages controls.
2257                         promise = api.Menus.insertAutoDraftPost( {
2258                                 post_title: title,
2259                                 post_type: 'page'
2260                         } );
2261                         promise.done( function( data ) {
2262                                 var availableItem, $content, itemTemplate;
2263
2264                                 // Prepare the new page as an available menu item.
2265                                 // See api.Menus.submitNew().
2266                                 availableItem = new api.Menus.AvailableItemModel( {
2267                                         'id': 'post-' + data.post_id, // Used for available menu item Backbone models.
2268                                         'title': title,
2269                                         'type': 'page',
2270                                         'type_label': api.Menus.data.l10n.page_label,
2271                                         'object': 'post_type',
2272                                         'object_id': data.post_id,
2273                                         'url': data.url
2274                                 } );
2275
2276                                 // Add the new item to the list of available menu items.
2277                                 api.Menus.availableMenuItemsPanel.collection.add( availableItem );
2278                                 $content = $( '#available-menu-items-post_type-page' ).find( '.available-menu-items-list' );
2279                                 itemTemplate = wp.template( 'available-menu-item' );
2280                                 $content.prepend( itemTemplate( availableItem.attributes ) );
2281
2282                                 // Focus the select control.
2283                                 select.focus();
2284                                 control.setting.set( String( data.post_id ) ); // Triggers a preview refresh and updates the setting.
2285
2286                                 // Reset the create page form.
2287                                 container.slideUp( 180 );
2288                                 toggle.slideDown( 180 );
2289                         } );
2290                         promise.always( function() {
2291                                 input.val( '' ).removeAttr( 'disabled' );
2292                         } );
2293                 }
2294         });
2295
2296         /**
2297          * A colorpicker control.
2298          *
2299          * @class
2300          * @augments wp.customize.Control
2301          * @augments wp.customize.Class
2302          */
2303         api.ColorControl = api.Control.extend({
2304                 ready: function() {
2305                         var control = this,
2306                                 isHueSlider = this.params.mode === 'hue',
2307                                 updating = false,
2308                                 picker;
2309
2310                         if ( isHueSlider ) {
2311                                 picker = this.container.find( '.color-picker-hue' );
2312                                 picker.val( control.setting() ).wpColorPicker({
2313                                         change: function( event, ui ) {
2314                                                 updating = true;
2315                                                 control.setting( ui.color.h() );
2316                                                 updating = false;
2317                                         }
2318                                 });
2319                         } else {
2320                                 picker = this.container.find( '.color-picker-hex' );
2321                                 picker.val( control.setting() ).wpColorPicker({
2322                                         change: function() {
2323                                                 updating = true;
2324                                                 control.setting.set( picker.wpColorPicker( 'color' ) );
2325                                                 updating = false;
2326                                         },
2327                                         clear: function() {
2328                                                 updating = true;
2329                                                 control.setting.set( '' );
2330                                                 updating = false;
2331                                         }
2332                                 });
2333                         }
2334
2335                         control.setting.bind( function ( value ) {
2336                                 // Bail if the update came from the control itself.
2337                                 if ( updating ) {
2338                                         return;
2339                                 }
2340                                 picker.val( value );
2341                                 picker.wpColorPicker( 'color', value );
2342                         } );
2343
2344                         // Collapse color picker when hitting Esc instead of collapsing the current section.
2345                         control.container.on( 'keydown', function( event ) {
2346                                 var pickerContainer;
2347                                 if ( 27 !== event.which ) { // Esc.
2348                                         return;
2349                                 }
2350                                 pickerContainer = control.container.find( '.wp-picker-container' );
2351                                 if ( pickerContainer.hasClass( 'wp-picker-active' ) ) {
2352                                         picker.wpColorPicker( 'close' );
2353                                         control.container.find( '.wp-color-result' ).focus();
2354                                         event.stopPropagation(); // Prevent section from being collapsed.
2355                                 }
2356                         } );
2357                 }
2358         });
2359
2360         /**
2361          * A control that implements the media modal.
2362          *
2363          * @class
2364          * @augments wp.customize.Control
2365          * @augments wp.customize.Class
2366          */
2367         api.MediaControl = api.Control.extend({
2368
2369                 /**
2370                  * When the control's DOM structure is ready,
2371                  * set up internal event bindings.
2372                  */
2373                 ready: function() {
2374                         var control = this;
2375                         // Shortcut so that we don't have to use _.bind every time we add a callback.
2376                         _.bindAll( control, 'restoreDefault', 'removeFile', 'openFrame', 'select', 'pausePlayer' );
2377
2378                         // Bind events, with delegation to facilitate re-rendering.
2379                         control.container.on( 'click keydown', '.upload-button', control.openFrame );
2380                         control.container.on( 'click keydown', '.upload-button', control.pausePlayer );
2381                         control.container.on( 'click keydown', '.thumbnail-image img', control.openFrame );
2382                         control.container.on( 'click keydown', '.default-button', control.restoreDefault );
2383                         control.container.on( 'click keydown', '.remove-button', control.pausePlayer );
2384                         control.container.on( 'click keydown', '.remove-button', control.removeFile );
2385                         control.container.on( 'click keydown', '.remove-button', control.cleanupPlayer );
2386
2387                         // Resize the player controls when it becomes visible (ie when section is expanded)
2388                         api.section( control.section() ).container
2389                                 .on( 'expanded', function() {
2390                                         if ( control.player ) {
2391                                                 control.player.setControlsSize();
2392                                         }
2393                                 })
2394                                 .on( 'collapsed', function() {
2395                                         control.pausePlayer();
2396                                 });
2397
2398                         /**
2399                          * Set attachment data and render content.
2400                          *
2401                          * Note that BackgroundImage.prototype.ready applies this ready method
2402                          * to itself. Since BackgroundImage is an UploadControl, the value
2403                          * is the attachment URL instead of the attachment ID. In this case
2404                          * we skip fetching the attachment data because we have no ID available,
2405                          * and it is the responsibility of the UploadControl to set the control's
2406                          * attachmentData before calling the renderContent method.
2407                          *
2408                          * @param {number|string} value Attachment
2409                          */
2410                         function setAttachmentDataAndRenderContent( value ) {
2411                                 var hasAttachmentData = $.Deferred();
2412
2413                                 if ( control.extended( api.UploadControl ) ) {
2414                                         hasAttachmentData.resolve();
2415                                 } else {
2416                                         value = parseInt( value, 10 );
2417                                         if ( _.isNaN( value ) || value <= 0 ) {
2418                                                 delete control.params.attachment;
2419                                                 hasAttachmentData.resolve();
2420                                         } else if ( control.params.attachment && control.params.attachment.id === value ) {
2421                                                 hasAttachmentData.resolve();
2422                                         }
2423                                 }
2424
2425                                 // Fetch the attachment data.
2426                                 if ( 'pending' === hasAttachmentData.state() ) {
2427                                         wp.media.attachment( value ).fetch().done( function() {
2428                                                 control.params.attachment = this.attributes;
2429                                                 hasAttachmentData.resolve();
2430
2431                                                 // Send attachment information to the preview for possible use in `postMessage` transport.
2432                                                 wp.customize.previewer.send( control.setting.id + '-attachment-data', this.attributes );
2433                                         } );
2434                                 }
2435
2436                                 hasAttachmentData.done( function() {
2437                                         control.renderContent();
2438                                 } );
2439                         }
2440
2441                         // Ensure attachment data is initially set (for dynamically-instantiated controls).
2442                         setAttachmentDataAndRenderContent( control.setting() );
2443
2444                         // Update the attachment data and re-render the control when the setting changes.
2445                         control.setting.bind( setAttachmentDataAndRenderContent );
2446                 },
2447
2448                 pausePlayer: function () {
2449                         this.player && this.player.pause();
2450                 },
2451
2452                 cleanupPlayer: function () {
2453                         this.player && wp.media.mixin.removePlayer( this.player );
2454                 },
2455
2456                 /**
2457                  * Open the media modal.
2458                  */
2459                 openFrame: function( event ) {
2460                         if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2461                                 return;
2462                         }
2463
2464                         event.preventDefault();
2465
2466                         if ( ! this.frame ) {
2467                                 this.initFrame();
2468                         }
2469
2470                         this.frame.open();
2471                 },
2472
2473                 /**
2474                  * Create a media modal select frame, and store it so the instance can be reused when needed.
2475                  */
2476                 initFrame: function() {
2477                         this.frame = wp.media({
2478                                 button: {
2479                                         text: this.params.button_labels.frame_button
2480                                 },
2481                                 states: [
2482                                         new wp.media.controller.Library({
2483                                                 title:     this.params.button_labels.frame_title,
2484                                                 library:   wp.media.query({ type: this.params.mime_type }),
2485                                                 multiple:  false,
2486                                                 date:      false
2487                                         })
2488                                 ]
2489                         });
2490
2491                         // When a file is selected, run a callback.
2492                         this.frame.on( 'select', this.select );
2493                 },
2494
2495                 /**
2496                  * Callback handler for when an attachment is selected in the media modal.
2497                  * Gets the selected image information, and sets it within the control.
2498                  */
2499                 select: function() {
2500                         // Get the attachment from the modal frame.
2501                         var node,
2502                                 attachment = this.frame.state().get( 'selection' ).first().toJSON(),
2503                                 mejsSettings = window._wpmejsSettings || {};
2504
2505                         this.params.attachment = attachment;
2506
2507                         // Set the Customizer setting; the callback takes care of rendering.
2508                         this.setting( attachment.id );
2509                         node = this.container.find( 'audio, video' ).get(0);
2510
2511                         // Initialize audio/video previews.
2512                         if ( node ) {
2513                                 this.player = new MediaElementPlayer( node, mejsSettings );
2514                         } else {
2515                                 this.cleanupPlayer();
2516                         }
2517                 },
2518
2519                 /**
2520                  * Reset the setting to the default value.
2521                  */
2522                 restoreDefault: function( event ) {
2523                         if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2524                                 return;
2525                         }
2526                         event.preventDefault();
2527
2528                         this.params.attachment = this.params.defaultAttachment;
2529                         this.setting( this.params.defaultAttachment.url );
2530                 },
2531
2532                 /**
2533                  * Called when the "Remove" link is clicked. Empties the setting.
2534                  *
2535                  * @param {object} event jQuery Event object
2536                  */
2537                 removeFile: function( event ) {
2538                         if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2539                                 return;
2540                         }
2541                         event.preventDefault();
2542
2543                         this.params.attachment = {};
2544                         this.setting( '' );
2545                         this.renderContent(); // Not bound to setting change when emptying.
2546                 }
2547         });
2548
2549         /**
2550          * An upload control, which utilizes the media modal.
2551          *
2552          * @class
2553          * @augments wp.customize.MediaControl
2554          * @augments wp.customize.Control
2555          * @augments wp.customize.Class
2556          */
2557         api.UploadControl = api.MediaControl.extend({
2558
2559                 /**
2560                  * Callback handler for when an attachment is selected in the media modal.
2561                  * Gets the selected image information, and sets it within the control.
2562                  */
2563                 select: function() {
2564                         // Get the attachment from the modal frame.
2565                         var node,
2566                                 attachment = this.frame.state().get( 'selection' ).first().toJSON(),
2567                                 mejsSettings = window._wpmejsSettings || {};
2568
2569                         this.params.attachment = attachment;
2570
2571                         // Set the Customizer setting; the callback takes care of rendering.
2572                         this.setting( attachment.url );
2573                         node = this.container.find( 'audio, video' ).get(0);
2574
2575                         // Initialize audio/video previews.
2576                         if ( node ) {
2577                                 this.player = new MediaElementPlayer( node, mejsSettings );
2578                         } else {
2579                                 this.cleanupPlayer();
2580                         }
2581                 },
2582
2583                 // @deprecated
2584                 success: function() {},
2585
2586                 // @deprecated
2587                 removerVisibility: function() {}
2588         });
2589
2590         /**
2591          * A control for uploading images.
2592          *
2593          * This control no longer needs to do anything more
2594          * than what the upload control does in JS.
2595          *
2596          * @class
2597          * @augments wp.customize.UploadControl
2598          * @augments wp.customize.MediaControl
2599          * @augments wp.customize.Control
2600          * @augments wp.customize.Class
2601          */
2602         api.ImageControl = api.UploadControl.extend({
2603                 // @deprecated
2604                 thumbnailSrc: function() {}
2605         });
2606
2607         /**
2608          * A control for uploading background images.
2609          *
2610          * @class
2611          * @augments wp.customize.UploadControl
2612          * @augments wp.customize.MediaControl
2613          * @augments wp.customize.Control
2614          * @augments wp.customize.Class
2615          */
2616         api.BackgroundControl = api.UploadControl.extend({
2617
2618                 /**
2619                  * When the control's DOM structure is ready,
2620                  * set up internal event bindings.
2621                  */
2622                 ready: function() {
2623                         api.UploadControl.prototype.ready.apply( this, arguments );
2624                 },
2625
2626                 /**
2627                  * Callback handler for when an attachment is selected in the media modal.
2628                  * Does an additional AJAX request for setting the background context.
2629                  */
2630                 select: function() {
2631                         api.UploadControl.prototype.select.apply( this, arguments );
2632
2633                         wp.ajax.post( 'custom-background-add', {
2634                                 nonce: _wpCustomizeBackground.nonces.add,
2635                                 wp_customize: 'on',
2636                                 customize_theme: api.settings.theme.stylesheet,
2637                                 attachment_id: this.params.attachment.id
2638                         } );
2639                 }
2640         });
2641
2642         /**
2643          * A control for positioning a background image.
2644          *
2645          * @since 4.7.0
2646          *
2647          * @class
2648          * @augments wp.customize.Control
2649          * @augments wp.customize.Class
2650          */
2651         api.BackgroundPositionControl = api.Control.extend( {
2652
2653                 /**
2654                  * Set up control UI once embedded in DOM and settings are created.
2655                  *
2656                  * @since 4.7.0
2657                  * @access public
2658                  */
2659                 ready: function() {
2660                         var control = this, updateRadios;
2661
2662                         control.container.on( 'change', 'input[name="background-position"]', function() {
2663                                 var position = $( this ).val().split( ' ' );
2664                                 control.settings.x( position[0] );
2665                                 control.settings.y( position[1] );
2666                         } );
2667
2668                         updateRadios = _.debounce( function() {
2669                                 var x, y, radioInput, inputValue;
2670                                 x = control.settings.x.get();
2671                                 y = control.settings.y.get();
2672                                 inputValue = String( x ) + ' ' + String( y );
2673                                 radioInput = control.container.find( 'input[name="background-position"][value="' + inputValue + '"]' );
2674                                 radioInput.click();
2675                         } );
2676                         control.settings.x.bind( updateRadios );
2677                         control.settings.y.bind( updateRadios );
2678
2679                         updateRadios(); // Set initial UI.
2680                 }
2681         } );
2682
2683         /**
2684          * A control for selecting and cropping an image.
2685          *
2686          * @class
2687          * @augments wp.customize.MediaControl
2688          * @augments wp.customize.Control
2689          * @augments wp.customize.Class
2690          */
2691         api.CroppedImageControl = api.MediaControl.extend({
2692
2693                 /**
2694                  * Open the media modal to the library state.
2695                  */
2696                 openFrame: function( event ) {
2697                         if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2698                                 return;
2699                         }
2700
2701                         this.initFrame();
2702                         this.frame.setState( 'library' ).open();
2703                 },
2704
2705                 /**
2706                  * Create a media modal select frame, and store it so the instance can be reused when needed.
2707                  */
2708                 initFrame: function() {
2709                         var l10n = _wpMediaViewsL10n;
2710
2711                         this.frame = wp.media({
2712                                 button: {
2713                                         text: l10n.select,
2714                                         close: false
2715                                 },
2716                                 states: [
2717                                         new wp.media.controller.Library({
2718                                                 title: this.params.button_labels.frame_title,
2719                                                 library: wp.media.query({ type: 'image' }),
2720                                                 multiple: false,
2721                                                 date: false,
2722                                                 priority: 20,
2723                                                 suggestedWidth: this.params.width,
2724                                                 suggestedHeight: this.params.height
2725                                         }),
2726                                         new wp.media.controller.CustomizeImageCropper({
2727                                                 imgSelectOptions: this.calculateImageSelectOptions,
2728                                                 control: this
2729                                         })
2730                                 ]
2731                         });
2732
2733                         this.frame.on( 'select', this.onSelect, this );
2734                         this.frame.on( 'cropped', this.onCropped, this );
2735                         this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
2736                 },
2737
2738                 /**
2739                  * After an image is selected in the media modal, switch to the cropper
2740                  * state if the image isn't the right size.
2741                  */
2742                 onSelect: function() {
2743                         var attachment = this.frame.state().get( 'selection' ).first().toJSON();
2744
2745                         if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
2746                                 this.setImageFromAttachment( attachment );
2747                                 this.frame.close();
2748                         } else {
2749                                 this.frame.setState( 'cropper' );
2750                         }
2751                 },
2752
2753                 /**
2754                  * After the image has been cropped, apply the cropped image data to the setting.
2755                  *
2756                  * @param {object} croppedImage Cropped attachment data.
2757                  */
2758                 onCropped: function( croppedImage ) {
2759                         this.setImageFromAttachment( croppedImage );
2760                 },
2761
2762                 /**
2763                  * Returns a set of options, computed from the attached image data and
2764                  * control-specific data, to be fed to the imgAreaSelect plugin in
2765                  * wp.media.view.Cropper.
2766                  *
2767                  * @param {wp.media.model.Attachment} attachment
2768                  * @param {wp.media.controller.Cropper} controller
2769                  * @returns {Object} Options
2770                  */
2771                 calculateImageSelectOptions: function( attachment, controller ) {
2772                         var control    = controller.get( 'control' ),
2773                                 flexWidth  = !! parseInt( control.params.flex_width, 10 ),
2774                                 flexHeight = !! parseInt( control.params.flex_height, 10 ),
2775                                 realWidth  = attachment.get( 'width' ),
2776                                 realHeight = attachment.get( 'height' ),
2777                                 xInit = parseInt( control.params.width, 10 ),
2778                                 yInit = parseInt( control.params.height, 10 ),
2779                                 ratio = xInit / yInit,
2780                                 xImg  = xInit,
2781                                 yImg  = yInit,
2782                                 x1, y1, imgSelectOptions;
2783
2784                         controller.set( 'canSkipCrop', ! control.mustBeCropped( flexWidth, flexHeight, xInit, yInit, realWidth, realHeight ) );
2785
2786                         if ( realWidth / realHeight > ratio ) {
2787                                 yInit = realHeight;
2788                                 xInit = yInit * ratio;
2789                         } else {
2790                                 xInit = realWidth;
2791                                 yInit = xInit / ratio;
2792                         }
2793
2794                         x1 = ( realWidth - xInit ) / 2;
2795                         y1 = ( realHeight - yInit ) / 2;
2796
2797                         imgSelectOptions = {
2798                                 handles: true,
2799                                 keys: true,
2800                                 instance: true,
2801                                 persistent: true,
2802                                 imageWidth: realWidth,
2803                                 imageHeight: realHeight,
2804                                 minWidth: xImg > xInit ? xInit : xImg,
2805                                 minHeight: yImg > yInit ? yInit : yImg,
2806                                 x1: x1,
2807                                 y1: y1,
2808                                 x2: xInit + x1,
2809                                 y2: yInit + y1
2810                         };
2811
2812                         if ( flexHeight === false && flexWidth === false ) {
2813                                 imgSelectOptions.aspectRatio = xInit + ':' + yInit;
2814                         }
2815
2816                         if ( true === flexHeight ) {
2817                                 delete imgSelectOptions.minHeight;
2818                                 imgSelectOptions.maxWidth = realWidth;
2819                         }
2820
2821                         if ( true === flexWidth ) {
2822                                 delete imgSelectOptions.minWidth;
2823                                 imgSelectOptions.maxHeight = realHeight;
2824                         }
2825
2826                         return imgSelectOptions;
2827                 },
2828
2829                 /**
2830                  * Return whether the image must be cropped, based on required dimensions.
2831                  *
2832                  * @param {bool} flexW
2833                  * @param {bool} flexH
2834                  * @param {int}  dstW
2835                  * @param {int}  dstH
2836                  * @param {int}  imgW
2837                  * @param {int}  imgH
2838                  * @return {bool}
2839                  */
2840                 mustBeCropped: function( flexW, flexH, dstW, dstH, imgW, imgH ) {
2841                         if ( true === flexW && true === flexH ) {
2842                                 return false;
2843                         }
2844
2845                         if ( true === flexW && dstH === imgH ) {
2846                                 return false;
2847                         }
2848
2849                         if ( true === flexH && dstW === imgW ) {
2850                                 return false;
2851                         }
2852
2853                         if ( dstW === imgW && dstH === imgH ) {
2854                                 return false;
2855                         }
2856
2857                         if ( imgW <= dstW ) {
2858                                 return false;
2859                         }
2860
2861                         return true;
2862                 },
2863
2864                 /**
2865                  * If cropping was skipped, apply the image data directly to the setting.
2866                  */
2867                 onSkippedCrop: function() {
2868                         var attachment = this.frame.state().get( 'selection' ).first().toJSON();
2869                         this.setImageFromAttachment( attachment );
2870                 },
2871
2872                 /**
2873                  * Updates the setting and re-renders the control UI.
2874                  *
2875                  * @param {object} attachment
2876                  */
2877                 setImageFromAttachment: function( attachment ) {
2878                         this.params.attachment = attachment;
2879
2880                         // Set the Customizer setting; the callback takes care of rendering.
2881                         this.setting( attachment.id );
2882                 }
2883         });
2884
2885         /**
2886          * A control for selecting and cropping Site Icons.
2887          *
2888          * @class
2889          * @augments wp.customize.CroppedImageControl
2890          * @augments wp.customize.MediaControl
2891          * @augments wp.customize.Control
2892          * @augments wp.customize.Class
2893          */
2894         api.SiteIconControl = api.CroppedImageControl.extend({
2895
2896                 /**
2897                  * Create a media modal select frame, and store it so the instance can be reused when needed.
2898                  */
2899                 initFrame: function() {
2900                         var l10n = _wpMediaViewsL10n;
2901
2902                         this.frame = wp.media({
2903                                 button: {
2904                                         text: l10n.select,
2905                                         close: false
2906                                 },
2907                                 states: [
2908                                         new wp.media.controller.Library({
2909                                                 title: this.params.button_labels.frame_title,
2910                                                 library: wp.media.query({ type: 'image' }),
2911                                                 multiple: false,
2912                                                 date: false,
2913                                                 priority: 20,
2914                                                 suggestedWidth: this.params.width,
2915                                                 suggestedHeight: this.params.height
2916                                         }),
2917                                         new wp.media.controller.SiteIconCropper({
2918                                                 imgSelectOptions: this.calculateImageSelectOptions,
2919                                                 control: this
2920                                         })
2921                                 ]
2922                         });
2923
2924                         this.frame.on( 'select', this.onSelect, this );
2925                         this.frame.on( 'cropped', this.onCropped, this );
2926                         this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
2927                 },
2928
2929                 /**
2930                  * After an image is selected in the media modal, switch to the cropper
2931                  * state if the image isn't the right size.
2932                  */
2933                 onSelect: function() {
2934                         var attachment = this.frame.state().get( 'selection' ).first().toJSON(),
2935                                 controller = this;
2936
2937                         if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
2938                                 wp.ajax.post( 'crop-image', {
2939                                         nonce: attachment.nonces.edit,
2940                                         id: attachment.id,
2941                                         context: 'site-icon',
2942                                         cropDetails: {
2943                                                 x1: 0,
2944                                                 y1: 0,
2945                                                 width: this.params.width,
2946                                                 height: this.params.height,
2947                                                 dst_width: this.params.width,
2948                                                 dst_height: this.params.height
2949                                         }
2950                                 } ).done( function( croppedImage ) {
2951                                         controller.setImageFromAttachment( croppedImage );
2952                                         controller.frame.close();
2953                                 } ).fail( function() {
2954                                         controller.frame.trigger('content:error:crop');
2955                                 } );
2956                         } else {
2957                                 this.frame.setState( 'cropper' );
2958                         }
2959                 },
2960
2961                 /**
2962                  * Updates the setting and re-renders the control UI.
2963                  *
2964                  * @param {object} attachment
2965                  */
2966                 setImageFromAttachment: function( attachment ) {
2967                         var sizes = [ 'site_icon-32', 'thumbnail', 'full' ], link,
2968                                 icon;
2969
2970                         _.each( sizes, function( size ) {
2971                                 if ( ! icon && ! _.isUndefined ( attachment.sizes[ size ] ) ) {
2972                                         icon = attachment.sizes[ size ];
2973                                 }
2974                         } );
2975
2976                         this.params.attachment = attachment;
2977
2978                         // Set the Customizer setting; the callback takes care of rendering.
2979                         this.setting( attachment.id );
2980
2981                         if ( ! icon ) {
2982                                 return;
2983                         }
2984
2985                         // Update the icon in-browser.
2986                         link = $( 'link[rel="icon"][sizes="32x32"]' );
2987                         link.attr( 'href', icon.url );
2988                 },
2989
2990                 /**
2991                  * Called when the "Remove" link is clicked. Empties the setting.
2992                  *
2993                  * @param {object} event jQuery Event object
2994                  */
2995                 removeFile: function( event ) {
2996                         if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2997                                 return;
2998                         }
2999                         event.preventDefault();
3000
3001                         this.params.attachment = {};
3002                         this.setting( '' );
3003                         this.renderContent(); // Not bound to setting change when emptying.
3004                         $( 'link[rel="icon"][sizes="32x32"]' ).attr( 'href', '/favicon.ico' ); // Set to default.
3005                 }
3006         });
3007
3008         /**
3009          * @class
3010          * @augments wp.customize.Control
3011          * @augments wp.customize.Class
3012          */
3013         api.HeaderControl = api.Control.extend({
3014                 ready: function() {
3015                         this.btnRemove = $('#customize-control-header_image .actions .remove');
3016                         this.btnNew    = $('#customize-control-header_image .actions .new');
3017
3018                         _.bindAll(this, 'openMedia', 'removeImage');
3019
3020                         this.btnNew.on( 'click', this.openMedia );
3021                         this.btnRemove.on( 'click', this.removeImage );
3022
3023                         api.HeaderTool.currentHeader = this.getInitialHeaderImage();
3024
3025                         new api.HeaderTool.CurrentView({
3026                                 model: api.HeaderTool.currentHeader,
3027                                 el: '#customize-control-header_image .current .container'
3028                         });
3029
3030                         new api.HeaderTool.ChoiceListView({
3031                                 collection: api.HeaderTool.UploadsList = new api.HeaderTool.ChoiceList(),
3032                                 el: '#customize-control-header_image .choices .uploaded .list'
3033                         });
3034
3035                         new api.HeaderTool.ChoiceListView({
3036                                 collection: api.HeaderTool.DefaultsList = new api.HeaderTool.DefaultsList(),
3037                                 el: '#customize-control-header_image .choices .default .list'
3038                         });
3039
3040                         api.HeaderTool.combinedList = api.HeaderTool.CombinedList = new api.HeaderTool.CombinedList([
3041                                 api.HeaderTool.UploadsList,
3042                                 api.HeaderTool.DefaultsList
3043                         ]);
3044
3045                         // Ensure custom-header-crop Ajax requests bootstrap the Customizer to activate the previewed theme.
3046                         wp.media.controller.Cropper.prototype.defaults.doCropArgs.wp_customize = 'on';
3047                         wp.media.controller.Cropper.prototype.defaults.doCropArgs.customize_theme = api.settings.theme.stylesheet;
3048                 },
3049
3050                 /**
3051                  * Returns a new instance of api.HeaderTool.ImageModel based on the currently
3052                  * saved header image (if any).
3053                  *
3054                  * @since 4.2.0
3055                  *
3056                  * @returns {Object} Options
3057                  */
3058                 getInitialHeaderImage: function() {
3059                         if ( ! api.get().header_image || ! api.get().header_image_data || _.contains( [ 'remove-header', 'random-default-image', 'random-uploaded-image' ], api.get().header_image ) ) {
3060                                 return new api.HeaderTool.ImageModel();
3061                         }
3062
3063                         // Get the matching uploaded image object.
3064                         var currentHeaderObject = _.find( _wpCustomizeHeader.uploads, function( imageObj ) {
3065                                 return ( imageObj.attachment_id === api.get().header_image_data.attachment_id );
3066                         } );
3067                         // Fall back to raw current header image.
3068                         if ( ! currentHeaderObject ) {
3069                                 currentHeaderObject = {
3070                                         url: api.get().header_image,
3071                                         thumbnail_url: api.get().header_image,
3072                                         attachment_id: api.get().header_image_data.attachment_id
3073                                 };
3074                         }
3075
3076                         return new api.HeaderTool.ImageModel({
3077                                 header: currentHeaderObject,
3078                                 choice: currentHeaderObject.url.split( '/' ).pop()
3079                         });
3080                 },
3081
3082                 /**
3083                  * Returns a set of options, computed from the attached image data and
3084                  * theme-specific data, to be fed to the imgAreaSelect plugin in
3085                  * wp.media.view.Cropper.
3086                  *
3087                  * @param {wp.media.model.Attachment} attachment
3088                  * @param {wp.media.controller.Cropper} controller
3089                  * @returns {Object} Options
3090                  */
3091                 calculateImageSelectOptions: function(attachment, controller) {
3092                         var xInit = parseInt(_wpCustomizeHeader.data.width, 10),
3093                                 yInit = parseInt(_wpCustomizeHeader.data.height, 10),
3094                                 flexWidth = !! parseInt(_wpCustomizeHeader.data['flex-width'], 10),
3095                                 flexHeight = !! parseInt(_wpCustomizeHeader.data['flex-height'], 10),
3096                                 ratio, xImg, yImg, realHeight, realWidth,
3097                                 imgSelectOptions;
3098
3099                         realWidth = attachment.get('width');
3100                         realHeight = attachment.get('height');
3101
3102                         this.headerImage = new api.HeaderTool.ImageModel();
3103                         this.headerImage.set({
3104                                 themeWidth: xInit,
3105                                 themeHeight: yInit,
3106                                 themeFlexWidth: flexWidth,
3107                                 themeFlexHeight: flexHeight,
3108                                 imageWidth: realWidth,
3109                                 imageHeight: realHeight
3110                         });
3111
3112                         controller.set( 'canSkipCrop', ! this.headerImage.shouldBeCropped() );
3113
3114                         ratio = xInit / yInit;
3115                         xImg = realWidth;
3116                         yImg = realHeight;
3117
3118                         if ( xImg / yImg > ratio ) {
3119                                 yInit = yImg;
3120                                 xInit = yInit * ratio;
3121                         } else {
3122                                 xInit = xImg;
3123                                 yInit = xInit / ratio;
3124                         }
3125
3126                         imgSelectOptions = {
3127                                 handles: true,
3128                                 keys: true,
3129                                 instance: true,
3130                                 persistent: true,
3131                                 imageWidth: realWidth,
3132                                 imageHeight: realHeight,
3133                                 x1: 0,
3134                                 y1: 0,
3135                                 x2: xInit,
3136                                 y2: yInit
3137                         };
3138
3139                         if (flexHeight === false && flexWidth === false) {
3140                                 imgSelectOptions.aspectRatio = xInit + ':' + yInit;
3141                         }
3142                         if (flexHeight === false ) {
3143                                 imgSelectOptions.maxHeight = yInit;
3144                         }
3145                         if (flexWidth === false ) {
3146                                 imgSelectOptions.maxWidth = xInit;
3147                         }
3148
3149                         return imgSelectOptions;
3150                 },
3151
3152                 /**
3153                  * Sets up and opens the Media Manager in order to select an image.
3154                  * Depending on both the size of the image and the properties of the
3155                  * current theme, a cropping step after selection may be required or
3156                  * skippable.
3157                  *
3158                  * @param {event} event
3159                  */
3160                 openMedia: function(event) {
3161                         var l10n = _wpMediaViewsL10n;
3162
3163                         event.preventDefault();
3164
3165                         this.frame = wp.media({
3166                                 button: {
3167                                         text: l10n.selectAndCrop,
3168                                         close: false
3169                                 },
3170                                 states: [
3171                                         new wp.media.controller.Library({
3172                                                 title:     l10n.chooseImage,
3173                                                 library:   wp.media.query({ type: 'image' }),
3174                                                 multiple:  false,
3175                                                 date:      false,
3176                                                 priority:  20,
3177                                                 suggestedWidth: _wpCustomizeHeader.data.width,
3178                                                 suggestedHeight: _wpCustomizeHeader.data.height
3179                                         }),
3180                                         new wp.media.controller.Cropper({
3181                                                 imgSelectOptions: this.calculateImageSelectOptions
3182                                         })
3183                                 ]
3184                         });
3185
3186                         this.frame.on('select', this.onSelect, this);
3187                         this.frame.on('cropped', this.onCropped, this);
3188                         this.frame.on('skippedcrop', this.onSkippedCrop, this);
3189
3190                         this.frame.open();
3191                 },
3192
3193                 /**
3194                  * After an image is selected in the media modal,
3195                  * switch to the cropper state.
3196                  */
3197                 onSelect: function() {
3198                         this.frame.setState('cropper');
3199                 },
3200
3201                 /**
3202                  * After the image has been cropped, apply the cropped image data to the setting.
3203                  *
3204                  * @param {object} croppedImage Cropped attachment data.
3205                  */
3206                 onCropped: function(croppedImage) {
3207                         var url = croppedImage.url,
3208                                 attachmentId = croppedImage.attachment_id,
3209                                 w = croppedImage.width,
3210                                 h = croppedImage.height;
3211                         this.setImageFromURL(url, attachmentId, w, h);
3212                 },
3213
3214                 /**
3215                  * If cropping was skipped, apply the image data directly to the setting.
3216                  *
3217                  * @param {object} selection
3218                  */
3219                 onSkippedCrop: function(selection) {
3220                         var url = selection.get('url'),
3221                                 w = selection.get('width'),
3222                                 h = selection.get('height');
3223                         this.setImageFromURL(url, selection.id, w, h);
3224                 },
3225
3226                 /**
3227                  * Creates a new wp.customize.HeaderTool.ImageModel from provided
3228                  * header image data and inserts it into the user-uploaded headers
3229                  * collection.
3230                  *
3231                  * @param {String} url
3232                  * @param {Number} attachmentId
3233                  * @param {Number} width
3234                  * @param {Number} height
3235                  */
3236                 setImageFromURL: function(url, attachmentId, width, height) {
3237                         var choice, data = {};
3238
3239                         data.url = url;
3240                         data.thumbnail_url = url;
3241                         data.timestamp = _.now();
3242
3243                         if (attachmentId) {
3244                                 data.attachment_id = attachmentId;
3245                         }
3246
3247                         if (width) {
3248                                 data.width = width;
3249                         }
3250
3251                         if (height) {
3252                                 data.height = height;
3253                         }
3254
3255                         choice = new api.HeaderTool.ImageModel({
3256                                 header: data,
3257                                 choice: url.split('/').pop()
3258                         });
3259                         api.HeaderTool.UploadsList.add(choice);
3260                         api.HeaderTool.currentHeader.set(choice.toJSON());
3261                         choice.save();
3262                         choice.importImage();
3263                 },
3264
3265                 /**
3266                  * Triggers the necessary events to deselect an image which was set as
3267                  * the currently selected one.
3268                  */
3269                 removeImage: function() {
3270                         api.HeaderTool.currentHeader.trigger('hide');
3271                         api.HeaderTool.CombinedList.trigger('control:removeImage');
3272                 }
3273
3274         });
3275
3276         /**
3277          * wp.customize.ThemeControl
3278          *
3279          * @constructor
3280          * @augments wp.customize.Control
3281          * @augments wp.customize.Class
3282          */
3283         api.ThemeControl = api.Control.extend({
3284
3285                 touchDrag: false,
3286                 isRendered: false,
3287
3288                 /**
3289                  * Defer rendering the theme control until the section is displayed.
3290                  *
3291                  * @since 4.2.0
3292                  */
3293                 renderContent: function () {
3294                         var control = this,
3295                                 renderContentArgs = arguments;
3296
3297                         api.section( control.section(), function( section ) {
3298                                 if ( section.expanded() ) {
3299                                         api.Control.prototype.renderContent.apply( control, renderContentArgs );
3300                                         control.isRendered = true;
3301                                 } else {
3302                                         section.expanded.bind( function( expanded ) {
3303                                                 if ( expanded && ! control.isRendered ) {
3304                                                         api.Control.prototype.renderContent.apply( control, renderContentArgs );
3305                                                         control.isRendered = true;
3306                                                 }
3307                                         } );
3308                                 }
3309                         } );
3310                 },
3311
3312                 /**
3313                  * @since 4.2.0
3314                  */
3315                 ready: function() {
3316                         var control = this;
3317
3318                         control.container.on( 'touchmove', '.theme', function() {
3319                                 control.touchDrag = true;
3320                         });
3321
3322                         // Bind details view trigger.
3323                         control.container.on( 'click keydown touchend', '.theme', function( event ) {
3324                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
3325                                         return;
3326                                 }
3327
3328                                 // Bail if the user scrolled on a touch device.
3329                                 if ( control.touchDrag === true ) {
3330                                         return control.touchDrag = false;
3331                                 }
3332
3333                                 // Prevent the modal from showing when the user clicks the action button.
3334                                 if ( $( event.target ).is( '.theme-actions .button' ) ) {
3335                                         return;
3336                                 }
3337
3338                                 api.section( control.section() ).loadThemePreview( control.params.theme.id );
3339                         });
3340
3341                         control.container.on( 'click keydown', '.theme-actions .theme-details', function( event ) {
3342                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
3343                                         return;
3344                                 }
3345
3346                                 event.preventDefault(); // Keep this AFTER the key filter above
3347
3348                                 api.section( control.section() ).showDetails( control.params.theme );
3349                         });
3350
3351                         control.container.on( 'render-screenshot', function() {
3352                                 var $screenshot = $( this ).find( 'img' ),
3353                                         source = $screenshot.data( 'src' );
3354
3355                                 if ( source ) {
3356                                         $screenshot.attr( 'src', source );
3357                                 }
3358                         });
3359                 },
3360
3361                 /**
3362                  * Show or hide the theme based on the presence of the term in the title, description, and author.
3363                  *
3364                  * @since 4.2.0
3365                  */
3366                 filter: function( term ) {
3367                         var control = this,
3368                                 haystack = control.params.theme.name + ' ' +
3369                                         control.params.theme.description + ' ' +
3370                                         control.params.theme.tags + ' ' +
3371                                         control.params.theme.author;
3372                         haystack = haystack.toLowerCase().replace( '-', ' ' );
3373                         if ( -1 !== haystack.search( term ) ) {
3374                                 control.activate();
3375                         } else {
3376                                 control.deactivate();
3377                         }
3378                 }
3379         });
3380
3381         // Change objects contained within the main customize object to Settings.
3382         api.defaultConstructor = api.Setting;
3383
3384         // Create the collections for Controls, Sections and Panels.
3385         api.control = new api.Values({ defaultConstructor: api.Control });
3386         api.section = new api.Values({ defaultConstructor: api.Section });
3387         api.panel = new api.Values({ defaultConstructor: api.Panel });
3388
3389         /**
3390          * An object that fetches a preview in the background of the document, which
3391          * allows for seamless replacement of an existing preview.
3392          *
3393          * @class
3394          * @augments wp.customize.Messenger
3395          * @augments wp.customize.Class
3396          * @mixes wp.customize.Events
3397          */
3398         api.PreviewFrame = api.Messenger.extend({
3399                 sensitivity: null, // Will get set to api.settings.timeouts.previewFrameSensitivity.
3400
3401                 /**
3402                  * Initialize the PreviewFrame.
3403                  *
3404                  * @param {object} params.container
3405                  * @param {object} params.previewUrl
3406                  * @param {object} params.query
3407                  * @param {object} options
3408                  */
3409                 initialize: function( params, options ) {
3410                         var deferred = $.Deferred();
3411
3412                         /*
3413                          * Make the instance of the PreviewFrame the promise object
3414                          * so other objects can easily interact with it.
3415                          */
3416                         deferred.promise( this );
3417
3418                         this.container = params.container;
3419
3420                         $.extend( params, { channel: api.PreviewFrame.uuid() });
3421
3422                         api.Messenger.prototype.initialize.call( this, params, options );
3423
3424                         this.add( 'previewUrl', params.previewUrl );
3425
3426                         this.query = $.extend( params.query || {}, { customize_messenger_channel: this.channel() });
3427
3428                         this.run( deferred );
3429                 },
3430
3431                 /**
3432                  * Run the preview request.
3433                  *
3434                  * @param {object} deferred jQuery Deferred object to be resolved with
3435                  *                          the request.
3436                  */
3437                 run: function( deferred ) {
3438                         var previewFrame = this,
3439                                 loaded = false,
3440                                 ready = false,
3441                                 readyData = null,
3442                                 hasPendingChangesetUpdate = '{}' !== previewFrame.query.customized,
3443                                 urlParser,
3444                                 params,
3445                                 form;
3446
3447                         if ( previewFrame._ready ) {
3448                                 previewFrame.unbind( 'ready', previewFrame._ready );
3449                         }
3450
3451                         previewFrame._ready = function( data ) {
3452                                 ready = true;
3453                                 readyData = data;
3454                                 previewFrame.container.addClass( 'iframe-ready' );
3455                                 if ( ! data ) {
3456                                         return;
3457                                 }
3458
3459                                 if ( loaded ) {
3460                                         deferred.resolveWith( previewFrame, [ data ] );
3461                                 }
3462                         };
3463
3464                         previewFrame.bind( 'ready', previewFrame._ready );
3465
3466                         urlParser = document.createElement( 'a' );
3467                         urlParser.href = previewFrame.previewUrl();
3468
3469                         params = _.extend(
3470                                 api.utils.parseQueryString( urlParser.search.substr( 1 ) ),
3471                                 {
3472                                         customize_changeset_uuid: previewFrame.query.customize_changeset_uuid,
3473                                         customize_theme: previewFrame.query.customize_theme,
3474                                         customize_messenger_channel: previewFrame.query.customize_messenger_channel
3475                                 }
3476                         );
3477
3478                         urlParser.search = $.param( params );
3479                         previewFrame.iframe = $( '<iframe />', {
3480                                 title: api.l10n.previewIframeTitle,
3481                                 name: 'customize-' + previewFrame.channel()
3482                         } );
3483                         previewFrame.iframe.attr( 'onmousewheel', '' ); // Workaround for Safari bug. See WP Trac #38149.
3484
3485                         if ( ! hasPendingChangesetUpdate ) {
3486                                 previewFrame.iframe.attr( 'src', urlParser.href );
3487                         } else {
3488                                 previewFrame.iframe.attr( 'data-src', urlParser.href ); // For debugging purposes.
3489                         }
3490
3491                         previewFrame.iframe.appendTo( previewFrame.container );
3492                         previewFrame.targetWindow( previewFrame.iframe[0].contentWindow );
3493
3494                         /*
3495                          * Submit customized data in POST request to preview frame window since
3496                          * there are setting value changes not yet written to changeset.
3497                          */
3498                         if ( hasPendingChangesetUpdate ) {
3499                                 form = $( '<form>', {
3500                                         action: urlParser.href,
3501                                         target: previewFrame.iframe.attr( 'name' ),
3502                                         method: 'post',
3503                                         hidden: 'hidden'
3504                                 } );
3505                                 form.append( $( '<input>', {
3506                                         type: 'hidden',
3507                                         name: '_method',
3508                                         value: 'GET'
3509                                 } ) );
3510                                 _.each( previewFrame.query, function( value, key ) {
3511                                         form.append( $( '<input>', {
3512                                                 type: 'hidden',
3513                                                 name: key,
3514                                                 value: value
3515                                         } ) );
3516                                 } );
3517                                 previewFrame.container.append( form );
3518                                 form.submit();
3519                                 form.remove(); // No need to keep the form around after submitted.
3520                         }
3521
3522                         previewFrame.bind( 'iframe-loading-error', function( error ) {
3523                                 previewFrame.iframe.remove();
3524
3525                                 // Check if the user is not logged in.
3526                                 if ( 0 === error ) {
3527                                         previewFrame.login( deferred );
3528                                         return;
3529                                 }
3530
3531                                 // Check for cheaters.
3532                                 if ( -1 === error ) {
3533                                         deferred.rejectWith( previewFrame, [ 'cheatin' ] );
3534                                         return;
3535                                 }
3536
3537                                 deferred.rejectWith( previewFrame, [ 'request failure' ] );
3538                         } );
3539
3540                         previewFrame.iframe.one( 'load', function() {
3541                                 loaded = true;
3542
3543                                 if ( ready ) {
3544                                         deferred.resolveWith( previewFrame, [ readyData ] );
3545                                 } else {
3546                                         setTimeout( function() {
3547                                                 deferred.rejectWith( previewFrame, [ 'ready timeout' ] );
3548                                         }, previewFrame.sensitivity );
3549                                 }
3550                         });
3551                 },
3552
3553                 login: function( deferred ) {
3554                         var self = this,
3555                                 reject;
3556
3557                         reject = function() {
3558                                 deferred.rejectWith( self, [ 'logged out' ] );
3559                         };
3560
3561                         if ( this.triedLogin ) {
3562                                 return reject();
3563                         }
3564
3565                         // Check if we have an admin cookie.
3566                         $.get( api.settings.url.ajax, {
3567                                 action: 'logged-in'
3568                         }).fail( reject ).done( function( response ) {
3569                                 var iframe;
3570
3571                                 if ( '1' !== response ) {
3572                                         reject();
3573                                 }
3574
3575                                 iframe = $( '<iframe />', { 'src': self.previewUrl(), 'title': api.l10n.previewIframeTitle } ).hide();
3576                                 iframe.appendTo( self.container );
3577                                 iframe.on( 'load', function() {
3578                                         self.triedLogin = true;
3579
3580                                         iframe.remove();
3581                                         self.run( deferred );
3582                                 });
3583                         });
3584                 },
3585
3586                 destroy: function() {
3587                         api.Messenger.prototype.destroy.call( this );
3588
3589                         if ( this.iframe ) {
3590                                 this.iframe.remove();
3591                         }
3592
3593                         delete this.iframe;
3594                         delete this.targetWindow;
3595                 }
3596         });
3597
3598         (function(){
3599                 var id = 0;
3600                 /**
3601                  * Return an incremented ID for a preview messenger channel.
3602                  *
3603                  * This function is named "uuid" for historical reasons, but it is a
3604                  * misnomer as it is not an actual UUID, and it is not universally unique.
3605                  * This is not to be confused with `api.settings.changeset.uuid`.
3606                  *
3607                  * @return {string}
3608                  */
3609                 api.PreviewFrame.uuid = function() {
3610                         return 'preview-' + String( id++ );
3611                 };
3612         }());
3613
3614         /**
3615          * Set the document title of the customizer.
3616          *
3617          * @since 4.1.0
3618          *
3619          * @param {string} documentTitle
3620          */
3621         api.setDocumentTitle = function ( documentTitle ) {
3622                 var tmpl, title;
3623                 tmpl = api.settings.documentTitleTmpl;
3624                 title = tmpl.replace( '%s', documentTitle );
3625                 document.title = title;
3626                 api.trigger( 'title', title );
3627         };
3628
3629         /**
3630          * @class
3631          * @augments wp.customize.Messenger
3632          * @augments wp.customize.Class
3633          * @mixes wp.customize.Events
3634          */
3635         api.Previewer = api.Messenger.extend({
3636                 refreshBuffer: null, // Will get set to api.settings.timeouts.windowRefresh.
3637
3638                 /**
3639                  * @param {array}  params.allowedUrls
3640                  * @param {string} params.container   A selector or jQuery element for the preview
3641                  *                                    frame to be placed.
3642                  * @param {string} params.form
3643                  * @param {string} params.previewUrl  The URL to preview.
3644                  * @param {object} options
3645                  */
3646                 initialize: function( params, options ) {
3647                         var previewer = this,
3648                                 urlParser = document.createElement( 'a' );
3649
3650                         $.extend( previewer, options || {} );
3651                         previewer.deferred = {
3652                                 active: $.Deferred()
3653                         };
3654
3655                         // Debounce to prevent hammering server and then wait for any pending update requests.
3656                         previewer.refresh = _.debounce(
3657                                 ( function( originalRefresh ) {
3658                                         return function() {
3659                                                 var isProcessingComplete, refreshOnceProcessingComplete;
3660                                                 isProcessingComplete = function() {
3661                                                         return 0 === api.state( 'processing' ).get();
3662                                                 };
3663                                                 if ( isProcessingComplete() ) {
3664                                                         originalRefresh.call( previewer );
3665                                                 } else {
3666                                                         refreshOnceProcessingComplete = function() {
3667                                                                 if ( isProcessingComplete() ) {
3668                                                                         originalRefresh.call( previewer );
3669                                                                         api.state( 'processing' ).unbind( refreshOnceProcessingComplete );
3670                                                                 }
3671                                                         };
3672                                                         api.state( 'processing' ).bind( refreshOnceProcessingComplete );
3673                                                 }
3674                                         };
3675                                 }( previewer.refresh ) ),
3676                                 previewer.refreshBuffer
3677                         );
3678
3679                         previewer.container   = api.ensure( params.container );
3680                         previewer.allowedUrls = params.allowedUrls;
3681
3682                         params.url = window.location.href;
3683
3684                         api.Messenger.prototype.initialize.call( previewer, params );
3685
3686                         urlParser.href = previewer.origin();
3687                         previewer.add( 'scheme', urlParser.protocol.replace( /:$/, '' ) );
3688
3689                         // Limit the URL to internal, front-end links.
3690                         //
3691                         // If the front end and the admin are served from the same domain, load the
3692                         // preview over ssl if the Customizer is being loaded over ssl. This avoids
3693                         // insecure content warnings. This is not attempted if the admin and front end
3694                         // are on different domains to avoid the case where the front end doesn't have
3695                         // ssl certs.
3696
3697                         previewer.add( 'previewUrl', params.previewUrl ).setter( function( to ) {
3698                                 var result = null, urlParser, queryParams, parsedAllowedUrl, parsedCandidateUrls = [];
3699                                 urlParser = document.createElement( 'a' );
3700                                 urlParser.href = to;
3701
3702                                 // Abort if URL is for admin or (static) files in wp-includes or wp-content.
3703                                 if ( /\/wp-(admin|includes|content)(\/|$)/.test( urlParser.pathname ) ) {
3704                                         return null;
3705                                 }
3706
3707                                 // Remove state query params.
3708                                 if ( urlParser.search.length > 1 ) {
3709                                         queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
3710                                         delete queryParams.customize_changeset_uuid;
3711                                         delete queryParams.customize_theme;
3712                                         delete queryParams.customize_messenger_channel;
3713                                         if ( _.isEmpty( queryParams ) ) {
3714                                                 urlParser.search = '';
3715                                         } else {
3716                                                 urlParser.search = $.param( queryParams );
3717                                         }
3718                                 }
3719
3720                                 parsedCandidateUrls.push( urlParser );
3721
3722                                 // Prepend list with URL that matches the scheme/protocol of the iframe.
3723                                 if ( previewer.scheme.get() + ':' !== urlParser.protocol ) {
3724                                         urlParser = document.createElement( 'a' );
3725                                         urlParser.href = parsedCandidateUrls[0].href;
3726                                         urlParser.protocol = previewer.scheme.get() + ':';
3727                                         parsedCandidateUrls.unshift( urlParser );
3728                                 }
3729
3730                                 // Attempt to match the URL to the control frame's scheme and check if it's allowed. If not, try the original URL.
3731                                 parsedAllowedUrl = document.createElement( 'a' );
3732                                 _.find( parsedCandidateUrls, function( parsedCandidateUrl ) {
3733                                         return ! _.isUndefined( _.find( previewer.allowedUrls, function( allowedUrl ) {
3734                                                 parsedAllowedUrl.href = allowedUrl;
3735                                                 if ( urlParser.protocol === parsedAllowedUrl.protocol && urlParser.host === parsedAllowedUrl.host && 0 === urlParser.pathname.indexOf( parsedAllowedUrl.pathname.replace( /\/$/, '' ) ) ) {
3736                                                         result = parsedCandidateUrl.href;
3737                                                         return true;
3738                                                 }
3739                                         } ) );
3740                                 } );
3741
3742                                 return result;
3743                         });
3744
3745                         previewer.bind( 'ready', previewer.ready );
3746
3747                         // Start listening for keep-alive messages when iframe first loads.
3748                         previewer.deferred.active.done( _.bind( previewer.keepPreviewAlive, previewer ) );
3749
3750                         previewer.bind( 'synced', function() {
3751                                 previewer.send( 'active' );
3752                         } );
3753
3754                         // Refresh the preview when the URL is changed (but not yet).
3755                         previewer.previewUrl.bind( previewer.refresh );
3756
3757                         previewer.scroll = 0;
3758                         previewer.bind( 'scroll', function( distance ) {
3759                                 previewer.scroll = distance;
3760                         });
3761
3762                         // Update the URL when the iframe sends a URL message, resetting scroll position. If URL is unchanged, then refresh.
3763                         previewer.bind( 'url', function( url ) {
3764                                 var onUrlChange, urlChanged = false;
3765                                 previewer.scroll = 0;
3766                                 onUrlChange = function() {
3767                                         urlChanged = true;
3768                                 };
3769                                 previewer.previewUrl.bind( onUrlChange );
3770                                 previewer.previewUrl.set( url );
3771                                 previewer.previewUrl.unbind( onUrlChange );
3772                                 if ( ! urlChanged ) {
3773                                         previewer.refresh();
3774                                 }
3775                         } );
3776
3777                         // Update the document title when the preview changes.
3778                         previewer.bind( 'documentTitle', function ( title ) {
3779                                 api.setDocumentTitle( title );
3780                         } );
3781                 },
3782
3783                 /**
3784                  * Handle the preview receiving the ready message.
3785                  *
3786                  * @since 4.7.0
3787                  * @access public
3788                  *
3789                  * @param {object} data - Data from preview.
3790                  * @param {string} data.currentUrl - Current URL.
3791                  * @param {object} data.activePanels - Active panels.
3792                  * @param {object} data.activeSections Active sections.
3793                  * @param {object} data.activeControls Active controls.
3794                  * @returns {void}
3795                  */
3796                 ready: function( data ) {
3797                         var previewer = this, synced = {}, constructs;
3798
3799                         synced.settings = api.get();
3800                         synced['settings-modified-while-loading'] = previewer.settingsModifiedWhileLoading;
3801                         if ( 'resolved' !== previewer.deferred.active.state() || previewer.loading ) {
3802                                 synced.scroll = previewer.scroll;
3803                         }
3804                         synced['edit-shortcut-visibility'] = api.state( 'editShortcutVisibility' ).get();
3805                         previewer.send( 'sync', synced );
3806
3807                         // Set the previewUrl without causing the url to set the iframe.
3808                         if ( data.currentUrl ) {
3809                                 previewer.previewUrl.unbind( previewer.refresh );
3810                                 previewer.previewUrl.set( data.currentUrl );
3811                                 previewer.previewUrl.bind( previewer.refresh );
3812                         }
3813
3814                         /*
3815                          * Walk over all panels, sections, and controls and set their
3816                          * respective active states to true if the preview explicitly
3817                          * indicates as such.
3818                          */
3819                         constructs = {
3820                                 panel: data.activePanels,
3821                                 section: data.activeSections,
3822                                 control: data.activeControls
3823                         };
3824                         _( constructs ).each( function ( activeConstructs, type ) {
3825                                 api[ type ].each( function ( construct, id ) {
3826                                         var isDynamicallyCreated = _.isUndefined( api.settings[ type + 's' ][ id ] );
3827
3828                                         /*
3829                                          * If the construct was created statically in PHP (not dynamically in JS)
3830                                          * then consider a missing (undefined) value in the activeConstructs to
3831                                          * mean it should be deactivated (since it is gone). But if it is
3832                                          * dynamically created then only toggle activation if the value is defined,
3833                                          * as this means that the construct was also then correspondingly
3834                                          * created statically in PHP and the active callback is available.
3835                                          * Otherwise, dynamically-created constructs should normally have
3836                                          * their active states toggled in JS rather than from PHP.
3837                                          */
3838                                         if ( ! isDynamicallyCreated || ! _.isUndefined( activeConstructs[ id ] ) ) {
3839                                                 if ( activeConstructs[ id ] ) {
3840                                                         construct.activate();
3841                                                 } else {
3842                                                         construct.deactivate();
3843                                                 }
3844                                         }
3845                                 } );
3846                         } );
3847
3848                         if ( data.settingValidities ) {
3849                                 api._handleSettingValidities( {
3850                                         settingValidities: data.settingValidities,
3851                                         focusInvalidControl: false
3852                                 } );
3853                         }
3854                 },
3855
3856                 /**
3857                  * Keep the preview alive by listening for ready and keep-alive messages.
3858                  *
3859                  * If a message is not received in the allotted time then the iframe will be set back to the last known valid URL.
3860                  *
3861                  * @since 4.7.0
3862                  * @access public
3863                  *
3864                  * @returns {void}
3865                  */
3866                 keepPreviewAlive: function keepPreviewAlive() {
3867                         var previewer = this, keepAliveTick, timeoutId, handleMissingKeepAlive, scheduleKeepAliveCheck;
3868
3869                         /**
3870                          * Schedule a preview keep-alive check.
3871                          *
3872                          * Note that if a page load takes longer than keepAliveCheck milliseconds,
3873                          * the keep-alive messages will still be getting sent from the previous
3874                          * URL.
3875                          */
3876                         scheduleKeepAliveCheck = function() {
3877                                 timeoutId = setTimeout( handleMissingKeepAlive, api.settings.timeouts.keepAliveCheck );
3878                         };
3879
3880                         /**
3881                          * Set the previewerAlive state to true when receiving a message from the preview.
3882                          */
3883                         keepAliveTick = function() {
3884                                 api.state( 'previewerAlive' ).set( true );
3885                                 clearTimeout( timeoutId );
3886                                 scheduleKeepAliveCheck();
3887                         };
3888
3889                         /**
3890                          * Set the previewerAlive state to false if keepAliveCheck milliseconds have transpired without a message.
3891                          *
3892                          * This is most likely to happen in the case of a connectivity error, or if the theme causes the browser
3893                          * to navigate to a non-allowed URL. Setting this state to false will force settings with a postMessage
3894                          * transport to use refresh instead, causing the preview frame also to be replaced with the current
3895                          * allowed preview URL.
3896                          */
3897                         handleMissingKeepAlive = function() {
3898                                 api.state( 'previewerAlive' ).set( false );
3899                         };
3900                         scheduleKeepAliveCheck();
3901
3902                         previewer.bind( 'ready', keepAliveTick );
3903                         previewer.bind( 'keep-alive', keepAliveTick );
3904                 },
3905
3906                 /**
3907                  * Query string data sent with each preview request.
3908                  *
3909                  * @abstract
3910                  */
3911                 query: function() {},
3912
3913                 abort: function() {
3914                         if ( this.loading ) {
3915                                 this.loading.destroy();
3916                                 delete this.loading;
3917                         }
3918                 },
3919
3920                 /**
3921                  * Refresh the preview seamlessly.
3922                  *
3923                  * @since 3.4.0
3924                  * @access public
3925                  * @returns {void}
3926                  */
3927                 refresh: function() {
3928                         var previewer = this, onSettingChange;
3929
3930                         // Display loading indicator
3931                         previewer.send( 'loading-initiated' );
3932
3933                         previewer.abort();
3934
3935                         previewer.loading = new api.PreviewFrame({
3936                                 url:        previewer.url(),
3937                                 previewUrl: previewer.previewUrl(),
3938                                 query:      previewer.query( { excludeCustomizedSaved: true } ) || {},
3939                                 container:  previewer.container
3940                         });
3941
3942                         previewer.settingsModifiedWhileLoading = {};
3943                         onSettingChange = function( setting ) {
3944                                 previewer.settingsModifiedWhileLoading[ setting.id ] = true;
3945                         };
3946                         api.bind( 'change', onSettingChange );
3947                         previewer.loading.always( function() {
3948                                 api.unbind( 'change', onSettingChange );
3949                         } );
3950
3951                         previewer.loading.done( function( readyData ) {
3952                                 var loadingFrame = this, onceSynced;
3953
3954                                 previewer.preview = loadingFrame;
3955                                 previewer.targetWindow( loadingFrame.targetWindow() );
3956                                 previewer.channel( loadingFrame.channel() );
3957
3958                                 onceSynced = function() {
3959                                         loadingFrame.unbind( 'synced', onceSynced );
3960                                         if ( previewer._previousPreview ) {
3961                                                 previewer._previousPreview.destroy();
3962                                         }
3963                                         previewer._previousPreview = previewer.preview;
3964                                         previewer.deferred.active.resolve();
3965                                         delete previewer.loading;
3966                                 };
3967                                 loadingFrame.bind( 'synced', onceSynced );
3968
3969                                 // This event will be received directly by the previewer in normal navigation; this is only needed for seamless refresh.
3970                                 previewer.trigger( 'ready', readyData );
3971                         });
3972
3973                         previewer.loading.fail( function( reason ) {
3974                                 previewer.send( 'loading-failed' );
3975
3976                                 if ( 'logged out' === reason ) {
3977                                         if ( previewer.preview ) {
3978                                                 previewer.preview.destroy();
3979                                                 delete previewer.preview;
3980                                         }
3981
3982                                         previewer.login().done( previewer.refresh );
3983                                 }
3984
3985                                 if ( 'cheatin' === reason ) {
3986                                         previewer.cheatin();
3987                                 }
3988                         });
3989                 },
3990
3991                 login: function() {
3992                         var previewer = this,
3993                                 deferred, messenger, iframe;
3994
3995                         if ( this._login )
3996                                 return this._login;
3997
3998                         deferred = $.Deferred();
3999                         this._login = deferred.promise();
4000
4001                         messenger = new api.Messenger({
4002                                 channel: 'login',
4003                                 url:     api.settings.url.login
4004                         });
4005
4006                         iframe = $( '<iframe />', { 'src': api.settings.url.login, 'title': api.l10n.loginIframeTitle } ).appendTo( this.container );
4007
4008                         messenger.targetWindow( iframe[0].contentWindow );
4009
4010                         messenger.bind( 'login', function () {
4011                                 var refreshNonces = previewer.refreshNonces();
4012
4013                                 refreshNonces.always( function() {
4014                                         iframe.remove();
4015                                         messenger.destroy();
4016                                         delete previewer._login;
4017                                 });
4018
4019                                 refreshNonces.done( function() {
4020                                         deferred.resolve();
4021                                 });
4022
4023                                 refreshNonces.fail( function() {
4024                                         previewer.cheatin();
4025                                         deferred.reject();
4026                                 });
4027                         });
4028
4029                         return this._login;
4030                 },
4031
4032                 cheatin: function() {
4033                         $( document.body ).empty().addClass( 'cheatin' ).append(
4034                                 '<h1>' + api.l10n.cheatin + '</h1>' +
4035                                 '<p>' + api.l10n.notAllowed + '</p>'
4036                         );
4037                 },
4038
4039                 refreshNonces: function() {
4040                         var request, deferred = $.Deferred();
4041
4042                         deferred.promise();
4043
4044                         request = wp.ajax.post( 'customize_refresh_nonces', {
4045                                 wp_customize: 'on',
4046                                 customize_theme: api.settings.theme.stylesheet
4047                         });
4048
4049                         request.done( function( response ) {
4050                                 api.trigger( 'nonce-refresh', response );
4051                                 deferred.resolve();
4052                         });
4053
4054                         request.fail( function() {
4055                                 deferred.reject();
4056                         });
4057
4058                         return deferred;
4059                 }
4060         });
4061
4062         api.settingConstructor = {};
4063         api.controlConstructor = {
4064                 color:               api.ColorControl,
4065                 media:               api.MediaControl,
4066                 upload:              api.UploadControl,
4067                 image:               api.ImageControl,
4068                 cropped_image:       api.CroppedImageControl,
4069                 site_icon:           api.SiteIconControl,
4070                 header:              api.HeaderControl,
4071                 background:          api.BackgroundControl,
4072                 background_position: api.BackgroundPositionControl,
4073                 theme:               api.ThemeControl
4074         };
4075         api.panelConstructor = {};
4076         api.sectionConstructor = {
4077                 themes: api.ThemesSection
4078         };
4079
4080         /**
4081          * Handle setting_validities in an error response for the customize-save request.
4082          *
4083          * Add notifications to the settings and focus on the first control that has an invalid setting.
4084          *
4085          * @since 4.6.0
4086          * @private
4087          *
4088          * @param {object}  args
4089          * @param {object}  args.settingValidities
4090          * @param {boolean} [args.focusInvalidControl=false]
4091          * @returns {void}
4092          */
4093         api._handleSettingValidities = function handleSettingValidities( args ) {
4094                 var invalidSettingControls, invalidSettings = [], wasFocused = false;
4095
4096                 // Find the controls that correspond to each invalid setting.
4097                 _.each( args.settingValidities, function( validity, settingId ) {
4098                         var setting = api( settingId );
4099                         if ( setting ) {
4100
4101                                 // Add notifications for invalidities.
4102                                 if ( _.isObject( validity ) ) {
4103                                         _.each( validity, function( params, code ) {
4104                                                 var notification, existingNotification, needsReplacement = false;
4105                                                 notification = new api.Notification( code, _.extend( { fromServer: true }, params ) );
4106
4107                                                 // Remove existing notification if already exists for code but differs in parameters.
4108                                                 existingNotification = setting.notifications( notification.code );
4109                                                 if ( existingNotification ) {
4110                                                         needsReplacement = notification.type !== existingNotification.type || notification.message !== existingNotification.message || ! _.isEqual( notification.data, existingNotification.data );
4111                                                 }
4112                                                 if ( needsReplacement ) {
4113                                                         setting.notifications.remove( code );
4114                                                 }
4115
4116                                                 if ( ! setting.notifications.has( notification.code ) ) {
4117                                                         setting.notifications.add( code, notification );
4118                                                 }
4119                                                 invalidSettings.push( setting.id );
4120                                         } );
4121                                 }
4122
4123                                 // Remove notification errors that are no longer valid.
4124                                 setting.notifications.each( function( notification ) {
4125                                         if ( 'error' === notification.type && ( true === validity || ! validity[ notification.code ] ) ) {
4126                                                 setting.notifications.remove( notification.code );
4127                                         }
4128                                 } );
4129                         }
4130                 } );
4131
4132                 if ( args.focusInvalidControl ) {
4133                         invalidSettingControls = api.findControlsForSettings( invalidSettings );
4134
4135                         // Focus on the first control that is inside of an expanded section (one that is visible).
4136                         _( _.values( invalidSettingControls ) ).find( function( controls ) {
4137                                 return _( controls ).find( function( control ) {
4138                                         var isExpanded = control.section() && api.section.has( control.section() ) && api.section( control.section() ).expanded();
4139                                         if ( isExpanded && control.expanded ) {
4140                                                 isExpanded = control.expanded();
4141                                         }
4142                                         if ( isExpanded ) {
4143                                                 control.focus();
4144                                                 wasFocused = true;
4145                                         }
4146                                         return wasFocused;
4147                                 } );
4148                         } );
4149
4150                         // Focus on the first invalid control.
4151                         if ( ! wasFocused && ! _.isEmpty( invalidSettingControls ) ) {
4152                                 _.values( invalidSettingControls )[0][0].focus();
4153                         }
4154                 }
4155         };
4156
4157         /**
4158          * Find all controls associated with the given settings.
4159          *
4160          * @since 4.6.0
4161          * @param {string[]} settingIds Setting IDs.
4162          * @returns {object<string, wp.customize.Control>} Mapping setting ids to arrays of controls.
4163          */
4164         api.findControlsForSettings = function findControlsForSettings( settingIds ) {
4165                 var controls = {}, settingControls;
4166                 _.each( _.unique( settingIds ), function( settingId ) {
4167                         var setting = api( settingId );
4168                         if ( setting ) {
4169                                 settingControls = setting.findControls();
4170                                 if ( settingControls && settingControls.length > 0 ) {
4171                                         controls[ settingId ] = settingControls;
4172                                 }
4173                         }
4174                 } );
4175                 return controls;
4176         };
4177
4178         /**
4179          * Sort panels, sections, controls by priorities. Hide empty sections and panels.
4180          *
4181          * @since 4.1.0
4182          */
4183         api.reflowPaneContents = _.bind( function () {
4184
4185                 var appendContainer, activeElement, rootHeadContainers, rootNodes = [], wasReflowed = false;
4186
4187                 if ( document.activeElement ) {
4188                         activeElement = $( document.activeElement );
4189                 }
4190
4191                 // Sort the sections within each panel
4192                 api.panel.each( function ( panel ) {
4193                         var sections = panel.sections(),
4194                                 sectionHeadContainers = _.pluck( sections, 'headContainer' );
4195                         rootNodes.push( panel );
4196                         appendContainer = ( panel.contentContainer.is( 'ul' ) ) ? panel.contentContainer : panel.contentContainer.find( 'ul:first' );
4197                         if ( ! api.utils.areElementListsEqual( sectionHeadContainers, appendContainer.children( '[id]' ) ) ) {
4198                                 _( sections ).each( function ( section ) {
4199                                         appendContainer.append( section.headContainer );
4200                                 } );
4201                                 wasReflowed = true;
4202                         }
4203                 } );
4204
4205                 // Sort the controls within each section
4206                 api.section.each( function ( section ) {
4207                         var controls = section.controls(),
4208                                 controlContainers = _.pluck( controls, 'container' );
4209                         if ( ! section.panel() ) {
4210                                 rootNodes.push( section );
4211                         }
4212                         appendContainer = ( section.contentContainer.is( 'ul' ) ) ? section.contentContainer : section.contentContainer.find( 'ul:first' );
4213                         if ( ! api.utils.areElementListsEqual( controlContainers, appendContainer.children( '[id]' ) ) ) {
4214                                 _( controls ).each( function ( control ) {
4215                                         appendContainer.append( control.container );
4216                                 } );
4217                                 wasReflowed = true;
4218                         }
4219                 } );
4220
4221                 // Sort the root panels and sections
4222                 rootNodes.sort( api.utils.prioritySort );
4223                 rootHeadContainers = _.pluck( rootNodes, 'headContainer' );
4224                 appendContainer = $( '#customize-theme-controls .customize-pane-parent' ); // @todo This should be defined elsewhere, and to be configurable
4225                 if ( ! api.utils.areElementListsEqual( rootHeadContainers, appendContainer.children() ) ) {
4226                         _( rootNodes ).each( function ( rootNode ) {
4227                                 appendContainer.append( rootNode.headContainer );
4228                         } );
4229                         wasReflowed = true;
4230                 }
4231
4232                 // Now re-trigger the active Value callbacks to that the panels and sections can decide whether they can be rendered
4233                 api.panel.each( function ( panel ) {
4234                         var value = panel.active();
4235                         panel.active.callbacks.fireWith( panel.active, [ value, value ] );
4236                 } );
4237                 api.section.each( function ( section ) {
4238                         var value = section.active();
4239                         section.active.callbacks.fireWith( section.active, [ value, value ] );
4240                 } );
4241
4242                 // Restore focus if there was a reflow and there was an active (focused) element
4243                 if ( wasReflowed && activeElement ) {
4244                         activeElement.focus();
4245                 }
4246                 api.trigger( 'pane-contents-reflowed' );
4247         }, api );
4248
4249         $( function() {
4250                 api.settings = window._wpCustomizeSettings;
4251                 api.l10n = window._wpCustomizeControlsL10n;
4252
4253                 // Check if we can run the Customizer.
4254                 if ( ! api.settings ) {
4255                         return;
4256                 }
4257
4258                 // Bail if any incompatibilities are found.
4259                 if ( ! $.support.postMessage || ( ! $.support.cors && api.settings.isCrossDomain ) ) {
4260                         return;
4261                 }
4262
4263                 if ( null === api.PreviewFrame.prototype.sensitivity ) {
4264                         api.PreviewFrame.prototype.sensitivity = api.settings.timeouts.previewFrameSensitivity;
4265                 }
4266                 if ( null === api.Previewer.prototype.refreshBuffer ) {
4267                         api.Previewer.prototype.refreshBuffer = api.settings.timeouts.windowRefresh;
4268                 }
4269
4270                 var parent,
4271                         body = $( document.body ),
4272                         overlay = body.children( '.wp-full-overlay' ),
4273                         title = $( '#customize-info .panel-title.site-title' ),
4274                         closeBtn = $( '.customize-controls-close' ),
4275                         saveBtn = $( '#save' ),
4276                         footerActions = $( '#customize-footer-actions' );
4277
4278                 // Prevent the form from saving when enter is pressed on an input or select element.
4279                 $('#customize-controls').on( 'keydown', function( e ) {
4280                         var isEnter = ( 13 === e.which ),
4281                                 $el = $( e.target );
4282
4283                         if ( isEnter && ( $el.is( 'input:not([type=button])' ) || $el.is( 'select' ) ) ) {
4284                                 e.preventDefault();
4285                         }
4286                 });
4287
4288                 // Expand/Collapse the main customizer customize info.
4289                 $( '.customize-info' ).find( '> .accordion-section-title .customize-help-toggle' ).on( 'click', function() {
4290                         var section = $( this ).closest( '.accordion-section' ),
4291                                 content = section.find( '.customize-panel-description:first' );
4292
4293                         if ( section.hasClass( 'cannot-expand' ) ) {
4294                                 return;
4295                         }
4296
4297                         if ( section.hasClass( 'open' ) ) {
4298                                 section.toggleClass( 'open' );
4299                                 content.slideUp( api.Panel.prototype.defaultExpandedArguments.duration );
4300                                 $( this ).attr( 'aria-expanded', false );
4301                         } else {
4302                                 content.slideDown( api.Panel.prototype.defaultExpandedArguments.duration );
4303                                 section.toggleClass( 'open' );
4304                                 $( this ).attr( 'aria-expanded', true );
4305                         }
4306                 });
4307
4308                 // Initialize Previewer
4309                 api.previewer = new api.Previewer({
4310                         container:   '#customize-preview',
4311                         form:        '#customize-controls',
4312                         previewUrl:  api.settings.url.preview,
4313                         allowedUrls: api.settings.url.allowed
4314                 }, {
4315
4316                         nonce: api.settings.nonce,
4317
4318                         /**
4319                          * Build the query to send along with the Preview request.
4320                          *
4321                          * @since 3.4.0
4322                          * @since 4.7.0 Added options param.
4323                          * @access public
4324                          *
4325                          * @param {object}  [options] Options.
4326                          * @param {boolean} [options.excludeCustomizedSaved=false] Exclude saved settings in customized response (values pending writing to changeset).
4327                          * @return {object} Query vars.
4328                          */
4329                         query: function( options ) {
4330                                 var queryVars = {
4331                                         wp_customize: 'on',
4332                                         customize_theme: api.settings.theme.stylesheet,
4333                                         nonce: this.nonce.preview,
4334                                         customize_changeset_uuid: api.settings.changeset.uuid
4335                                 };
4336
4337                                 /*
4338                                  * Exclude customized data if requested especially for calls to requestChangesetUpdate.
4339                                  * Changeset updates are differential and so it is a performance waste to send all of
4340                                  * the dirty settings with each update.
4341                                  */
4342                                 queryVars.customized = JSON.stringify( api.dirtyValues( {
4343                                         unsaved: options && options.excludeCustomizedSaved
4344                                 } ) );
4345
4346                                 return queryVars;
4347                         },
4348
4349                         /**
4350                          * Save (and publish) the customizer changeset.
4351                          *
4352                          * Updates to the changeset are transactional. If any of the settings
4353                          * are invalid then none of them will be written into the changeset.
4354                          * A revision will be made for the changeset post if revisions support
4355                          * has been added to the post type.
4356                          *
4357                          * @since 3.4.0
4358                          * @since 4.7.0 Added args param and return value.
4359                          *
4360                          * @param {object} [args] Args.
4361                          * @param {string} [args.status=publish] Status.
4362                          * @param {string} [args.date] Date, in local time in MySQL format.
4363                          * @param {string} [args.title] Title
4364                          * @returns {jQuery.promise} Promise.
4365                          */
4366                         save: function( args ) {
4367                                 var previewer = this,
4368                                         deferred = $.Deferred(),
4369                                         changesetStatus = 'publish',
4370                                         processing = api.state( 'processing' ),
4371                                         submitWhenDoneProcessing,
4372                                         submit,
4373                                         modifiedWhileSaving = {},
4374                                         invalidSettings = [],
4375                                         invalidControls;
4376
4377                                 if ( args && args.status ) {
4378                                         changesetStatus = args.status;
4379                                 }
4380
4381                                 if ( api.state( 'saving' ).get() ) {
4382                                         deferred.reject( 'already_saving' );
4383                                         deferred.promise();
4384                                 }
4385
4386                                 api.state( 'saving' ).set( true );
4387
4388                                 function captureSettingModifiedDuringSave( setting ) {
4389                                         modifiedWhileSaving[ setting.id ] = true;
4390                                 }
4391                                 api.bind( 'change', captureSettingModifiedDuringSave );
4392
4393                                 submit = function () {
4394                                         var request, query, settingInvalidities = {}, latestRevision = api._latestRevision;
4395
4396                                         /*
4397                                          * Block saving if there are any settings that are marked as
4398                                          * invalid from the client (not from the server). Focus on
4399                                          * the control.
4400                                          */
4401                                         api.each( function( setting ) {
4402                                                 setting.notifications.each( function( notification ) {
4403                                                         if ( 'error' === notification.type && ! notification.fromServer ) {
4404                                                                 invalidSettings.push( setting.id );
4405                                                                 if ( ! settingInvalidities[ setting.id ] ) {
4406                                                                         settingInvalidities[ setting.id ] = {};
4407                                                                 }
4408                                                                 settingInvalidities[ setting.id ][ notification.code ] = notification;
4409                                                         }
4410                                                 } );
4411                                         } );
4412                                         invalidControls = api.findControlsForSettings( invalidSettings );
4413                                         if ( ! _.isEmpty( invalidControls ) ) {
4414                                                 _.values( invalidControls )[0][0].focus();
4415                                                 api.unbind( 'change', captureSettingModifiedDuringSave );
4416                                                 deferred.rejectWith( previewer, [
4417                                                         { setting_invalidities: settingInvalidities }
4418                                                 ] );
4419                                                 api.state( 'saving' ).set( false );
4420                                                 return deferred.promise();
4421                                         }
4422
4423                                         /*
4424                                          * Note that excludeCustomizedSaved is intentionally false so that the entire
4425                                          * set of customized data will be included if bypassed changeset update.
4426                                          */
4427                                         query = $.extend( previewer.query( { excludeCustomizedSaved: false } ), {
4428                                                 nonce: previewer.nonce.save,
4429                                                 customize_changeset_status: changesetStatus
4430                                         } );
4431                                         if ( args && args.date ) {
4432                                                 query.customize_changeset_date = args.date;
4433                                         }
4434                                         if ( args && args.title ) {
4435                                                 query.customize_changeset_title = args.title;
4436                                         }
4437
4438                                         /*
4439                                          * Note that the dirty customized values will have already been set in the
4440                                          * changeset and so technically query.customized could be deleted. However,
4441                                          * it is remaining here to make sure that any settings that got updated
4442                                          * quietly which may have not triggered an update request will also get
4443                                          * included in the values that get saved to the changeset. This will ensure
4444                                          * that values that get injected via the saved event will be included in
4445                                          * the changeset. This also ensures that setting values that were invalid
4446                                          * will get re-validated, perhaps in the case of settings that are invalid
4447                                          * due to dependencies on other settings.
4448                                          */
4449                                         request = wp.ajax.post( 'customize_save', query );
4450
4451                                         // Disable save button during the save request.
4452                                         saveBtn.prop( 'disabled', true );
4453
4454                                         api.trigger( 'save', request );
4455
4456                                         request.always( function () {
4457                                                 api.state( 'saving' ).set( false );
4458                                                 saveBtn.prop( 'disabled', false );
4459                                                 api.unbind( 'change', captureSettingModifiedDuringSave );
4460                                         } );
4461
4462                                         request.fail( function ( response ) {
4463
4464                                                 if ( '0' === response ) {
4465                                                         response = 'not_logged_in';
4466                                                 } else if ( '-1' === response ) {
4467                                                         // Back-compat in case any other check_ajax_referer() call is dying
4468                                                         response = 'invalid_nonce';
4469                                                 }
4470
4471                                                 if ( 'invalid_nonce' === response ) {
4472                                                         previewer.cheatin();
4473                                                 } else if ( 'not_logged_in' === response ) {
4474                                                         previewer.preview.iframe.hide();
4475                                                         previewer.login().done( function() {
4476                                                                 previewer.save();
4477                                                                 previewer.preview.iframe.show();
4478                                                         } );
4479                                                 }
4480
4481                                                 if ( response.setting_validities ) {
4482                                                         api._handleSettingValidities( {
4483                                                                 settingValidities: response.setting_validities,
4484                                                                 focusInvalidControl: true
4485                                                         } );
4486                                                 }
4487
4488                                                 deferred.rejectWith( previewer, [ response ] );
4489                                                 api.trigger( 'error', response );
4490                                         } );
4491
4492                                         request.done( function( response ) {
4493
4494                                                 previewer.send( 'saved', response );
4495
4496                                                 api.state( 'changesetStatus' ).set( response.changeset_status );
4497                                                 if ( 'publish' === response.changeset_status ) {
4498
4499                                                         // Mark all published as clean if they haven't been modified during the request.
4500                                                         api.each( function( setting ) {
4501                                                                 /*
4502                                                                  * Note that the setting revision will be undefined in the case of setting
4503                                                                  * values that are marked as dirty when the customizer is loaded, such as
4504                                                                  * when applying starter content. All other dirty settings will have an
4505                                                                  * associated revision due to their modification triggering a change event.
4506                                                                  */
4507                                                                 if ( setting._dirty && ( _.isUndefined( api._latestSettingRevisions[ setting.id ] ) || api._latestSettingRevisions[ setting.id ] <= latestRevision ) ) {
4508                                                                         setting._dirty = false;
4509                                                                 }
4510                                                         } );
4511
4512                                                         api.state( 'changesetStatus' ).set( '' );
4513                                                         api.settings.changeset.uuid = response.next_changeset_uuid;
4514                                                         parent.send( 'changeset-uuid', api.settings.changeset.uuid );
4515                                                 }
4516
4517                                                 if ( response.setting_validities ) {
4518                                                         api._handleSettingValidities( {
4519                                                                 settingValidities: response.setting_validities,
4520                                                                 focusInvalidControl: true
4521                                                         } );
4522                                                 }
4523
4524                                                 deferred.resolveWith( previewer, [ response ] );
4525                                                 api.trigger( 'saved', response );
4526
4527                                                 // Restore the global dirty state if any settings were modified during save.
4528                                                 if ( ! _.isEmpty( modifiedWhileSaving ) ) {
4529                                                         api.state( 'saved' ).set( false );
4530                                                 }
4531                                         } );
4532                                 };
4533
4534                                 if ( 0 === processing() ) {
4535                                         submit();
4536                                 } else {
4537                                         submitWhenDoneProcessing = function () {
4538                                                 if ( 0 === processing() ) {
4539                                                         api.state.unbind( 'change', submitWhenDoneProcessing );
4540                                                         submit();
4541                                                 }
4542                                         };
4543                                         api.state.bind( 'change', submitWhenDoneProcessing );
4544                                 }
4545
4546                                 return deferred.promise();
4547                         }
4548                 });
4549
4550                 // Refresh the nonces if the preview sends updated nonces over.
4551                 api.previewer.bind( 'nonce', function( nonce ) {
4552                         $.extend( this.nonce, nonce );
4553                 });
4554
4555                 // Refresh the nonces if login sends updated nonces over.
4556                 api.bind( 'nonce-refresh', function( nonce ) {
4557                         $.extend( api.settings.nonce, nonce );
4558                         $.extend( api.previewer.nonce, nonce );
4559                         api.previewer.send( 'nonce-refresh', nonce );
4560                 });
4561
4562                 // Create Settings
4563                 $.each( api.settings.settings, function( id, data ) {
4564                         var constructor = api.settingConstructor[ data.type ] || api.Setting,
4565                                 setting;
4566
4567                         setting = new constructor( id, data.value, {
4568                                 transport: data.transport,
4569                                 previewer: api.previewer,
4570                                 dirty: !! data.dirty
4571                         } );
4572                         api.add( id, setting );
4573                 });
4574
4575                 // Create Panels
4576                 $.each( api.settings.panels, function ( id, data ) {
4577                         var constructor = api.panelConstructor[ data.type ] || api.Panel,
4578                                 panel;
4579
4580                         panel = new constructor( id, {
4581                                 params: data
4582                         } );
4583                         api.panel.add( id, panel );
4584                 });
4585
4586                 // Create Sections
4587                 $.each( api.settings.sections, function ( id, data ) {
4588                         var constructor = api.sectionConstructor[ data.type ] || api.Section,
4589                                 section;
4590
4591                         section = new constructor( id, {
4592                                 params: data
4593                         } );
4594                         api.section.add( id, section );
4595                 });
4596
4597                 // Create Controls
4598                 $.each( api.settings.controls, function( id, data ) {
4599                         var constructor = api.controlConstructor[ data.type ] || api.Control,
4600                                 control;
4601
4602                         control = new constructor( id, {
4603                                 params: data,
4604                                 previewer: api.previewer
4605                         } );
4606                         api.control.add( id, control );
4607                 });
4608
4609                 // Focus the autofocused element
4610                 _.each( [ 'panel', 'section', 'control' ], function( type ) {
4611                         var id = api.settings.autofocus[ type ];
4612                         if ( ! id ) {
4613                                 return;
4614                         }
4615
4616                         /*
4617                          * Defer focus until:
4618                          * 1. The panel, section, or control exists (especially for dynamically-created ones).
4619                          * 2. The instance is embedded in the document (and so is focusable).
4620                          * 3. The preview has finished loading so that the active states have been set.
4621                          */
4622                         api[ type ]( id, function( instance ) {
4623                                 instance.deferred.embedded.done( function() {
4624                                         api.previewer.deferred.active.done( function() {
4625                                                 instance.focus();
4626                                         });
4627                                 });
4628                         });
4629                 });
4630
4631                 api.bind( 'ready', api.reflowPaneContents );
4632                 $( [ api.panel, api.section, api.control ] ).each( function ( i, values ) {
4633                         var debouncedReflowPaneContents = _.debounce( api.reflowPaneContents, api.settings.timeouts.reflowPaneContents );
4634                         values.bind( 'add', debouncedReflowPaneContents );
4635                         values.bind( 'change', debouncedReflowPaneContents );
4636                         values.bind( 'remove', debouncedReflowPaneContents );
4637                 } );
4638
4639                 // Save and activated states
4640                 (function() {
4641                         var state = new api.Values(),
4642                                 saved = state.create( 'saved' ),
4643                                 saving = state.create( 'saving' ),
4644                                 activated = state.create( 'activated' ),
4645                                 processing = state.create( 'processing' ),
4646                                 paneVisible = state.create( 'paneVisible' ),
4647                                 expandedPanel = state.create( 'expandedPanel' ),
4648                                 expandedSection = state.create( 'expandedSection' ),
4649                                 changesetStatus = state.create( 'changesetStatus' ),
4650                                 previewerAlive = state.create( 'previewerAlive' ),
4651                                 editShortcutVisibility  = state.create( 'editShortcutVisibility' ),
4652                                 populateChangesetUuidParam;
4653
4654                         state.bind( 'change', function() {
4655                                 var canSave;
4656
4657                                 if ( ! activated() ) {
4658                                         saveBtn.val( api.l10n.activate );
4659                                         closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
4660
4661                                 } else if ( '' === changesetStatus.get() && saved() ) {
4662                                         saveBtn.val( api.l10n.saved );
4663                                         closeBtn.find( '.screen-reader-text' ).text( api.l10n.close );
4664
4665                                 } else {
4666                                         saveBtn.val( api.l10n.save );
4667                                         closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
4668                                 }
4669
4670                                 /*
4671                                  * Save (publish) button should be enabled if saving is not currently happening,
4672                                  * and if the theme is not active or the changeset exists but is not published.
4673                                  */
4674                                 canSave = ! saving() && ( ! activated() || ! saved() || ( '' !== changesetStatus() && 'publish' !== changesetStatus() ) );
4675
4676                                 saveBtn.prop( 'disabled', ! canSave );
4677                         });
4678
4679                         // Set default states.
4680                         changesetStatus( api.settings.changeset.status );
4681                         saved( true );
4682                         if ( '' === changesetStatus() ) { // Handle case for loading starter content.
4683                                 api.each( function( setting ) {
4684                                         if ( setting._dirty ) {
4685                                                 saved( false );
4686                                         }
4687                                 } );
4688                         }
4689                         saving( false );
4690                         activated( api.settings.theme.active );
4691                         processing( 0 );
4692                         paneVisible( true );
4693                         expandedPanel( false );
4694                         expandedSection( false );
4695                         previewerAlive( true );
4696                         editShortcutVisibility( 'visible' );
4697
4698                         api.bind( 'change', function() {
4699                                 state('saved').set( false );
4700                         });
4701
4702                         saving.bind( function( isSaving ) {
4703                                 body.toggleClass( 'saving', isSaving );
4704                         } );
4705
4706                         api.bind( 'saved', function( response ) {
4707                                 state('saved').set( true );
4708                                 if ( 'publish' === response.changeset_status ) {
4709                                         state( 'activated' ).set( true );
4710                                 }
4711                         });
4712
4713                         activated.bind( function( to ) {
4714                                 if ( to ) {
4715                                         api.trigger( 'activated' );
4716                                 }
4717                         });
4718
4719                         /**
4720                          * Populate URL with UUID via `history.replaceState()`.
4721                          *
4722                          * @since 4.7.0
4723                          * @access private
4724                          *
4725                          * @param {boolean} isIncluded Is UUID included.
4726                          * @returns {void}
4727                          */
4728                         populateChangesetUuidParam = function( isIncluded ) {
4729                                 var urlParser, queryParams;
4730                                 urlParser = document.createElement( 'a' );
4731                                 urlParser.href = location.href;
4732                                 queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
4733                                 if ( isIncluded ) {
4734                                         if ( queryParams.changeset_uuid === api.settings.changeset.uuid ) {
4735                                                 return;
4736                                         }
4737                                         queryParams.changeset_uuid = api.settings.changeset.uuid;
4738                                 } else {
4739                                         if ( ! queryParams.changeset_uuid ) {
4740                                                 return;
4741                                         }
4742                                         delete queryParams.changeset_uuid;
4743                                 }
4744                                 urlParser.search = $.param( queryParams );
4745                                 history.replaceState( {}, document.title, urlParser.href );
4746                         };
4747
4748                         if ( history.replaceState ) {
4749                                 changesetStatus.bind( function( newStatus ) {
4750                                         populateChangesetUuidParam( '' !== newStatus && 'publish' !== newStatus );
4751                                 } );
4752                         }
4753
4754                         // Expose states to the API.
4755                         api.state = state;
4756                 }());
4757
4758                 // Check if preview url is valid and load the preview frame.
4759                 if ( api.previewer.previewUrl() ) {
4760                         api.previewer.refresh();
4761                 } else {
4762                         api.previewer.previewUrl( api.settings.url.home );
4763                 }
4764
4765                 // Button bindings.
4766                 saveBtn.click( function( event ) {
4767                         api.previewer.save();
4768                         event.preventDefault();
4769                 }).keydown( function( event ) {
4770                         if ( 9 === event.which ) // tab
4771                                 return;
4772                         if ( 13 === event.which ) // enter
4773                                 api.previewer.save();
4774                         event.preventDefault();
4775                 });
4776
4777                 closeBtn.keydown( function( event ) {
4778                         if ( 9 === event.which ) // tab
4779                                 return;
4780                         if ( 13 === event.which ) // enter
4781                                 this.click();
4782                         event.preventDefault();
4783                 });
4784
4785                 $( '.collapse-sidebar' ).on( 'click', function() {
4786                         api.state( 'paneVisible' ).set( ! api.state( 'paneVisible' ).get() );
4787                 });
4788
4789                 api.state( 'paneVisible' ).bind( function( paneVisible ) {
4790                         overlay.toggleClass( 'preview-only', ! paneVisible );
4791                         overlay.toggleClass( 'expanded', paneVisible );
4792                         overlay.toggleClass( 'collapsed', ! paneVisible );
4793
4794                         if ( ! paneVisible ) {
4795                                 $( '.collapse-sidebar' ).attr({ 'aria-expanded': 'false', 'aria-label': api.l10n.expandSidebar });
4796                         } else {
4797                                 $( '.collapse-sidebar' ).attr({ 'aria-expanded': 'true', 'aria-label': api.l10n.collapseSidebar });
4798                         }
4799                 });
4800
4801                 // Keyboard shortcuts - esc to exit section/panel.
4802                 $( 'body' ).on( 'keydown', function( event ) {
4803                         var collapsedObject, expandedControls = [], expandedSections = [], expandedPanels = [];
4804
4805                         if ( 27 !== event.which ) { // Esc.
4806                                 return;
4807                         }
4808
4809                         /*
4810                          * Abort if the event target is not the body (the default) and not inside of #customize-controls.
4811                          * This ensures that ESC meant to collapse a modal dialog or a TinyMCE toolbar won't collapse something else.
4812                          */
4813                         if ( ! $( event.target ).is( 'body' ) && ! $.contains( $( '#customize-controls' )[0], event.target ) ) {
4814                                 return;
4815                         }
4816
4817                         // Check for expanded expandable controls (e.g. widgets and nav menus items), sections, and panels.
4818                         api.control.each( function( control ) {
4819                                 if ( control.expanded && control.expanded() && _.isFunction( control.collapse ) ) {
4820                                         expandedControls.push( control );
4821                                 }
4822                         });
4823                         api.section.each( function( section ) {
4824                                 if ( section.expanded() ) {
4825                                         expandedSections.push( section );
4826                                 }
4827                         });
4828                         api.panel.each( function( panel ) {
4829                                 if ( panel.expanded() ) {
4830                                         expandedPanels.push( panel );
4831                                 }
4832                         });
4833
4834                         // Skip collapsing expanded controls if there are no expanded sections.
4835                         if ( expandedControls.length > 0 && 0 === expandedSections.length ) {
4836                                 expandedControls.length = 0;
4837                         }
4838
4839                         // Collapse the most granular expanded object.
4840                         collapsedObject = expandedControls[0] || expandedSections[0] || expandedPanels[0];
4841                         if ( collapsedObject ) {
4842                                 collapsedObject.collapse();
4843                                 event.preventDefault();
4844                         }
4845                 });
4846
4847                 $( '.customize-controls-preview-toggle' ).on( 'click', function() {
4848                         api.state( 'paneVisible' ).set( ! api.state( 'paneVisible' ).get() );
4849                 });
4850
4851                 /*
4852                  * Sticky header feature.
4853                  */
4854                 (function initStickyHeaders() {
4855                         var parentContainer = $( '.wp-full-overlay-sidebar-content' ),
4856                                 changeContainer, getHeaderHeight, releaseStickyHeader, resetStickyHeader, positionStickyHeader,
4857                                 activeHeader, lastScrollTop;
4858
4859                         /**
4860                          * Determine which panel or section is currently expanded.
4861                          *
4862                          * @since 4.7.0
4863                          * @access private
4864                          *
4865                          * @param {wp.customize.Panel|wp.customize.Section} container Construct.
4866                          * @returns {void}
4867                          */
4868                         changeContainer = function( container ) {
4869                                 var newInstance = container,
4870                                         expandedSection = api.state( 'expandedSection' ).get(),
4871                                         expandedPanel = api.state( 'expandedPanel' ).get(),
4872                                         headerElement;
4873
4874                                 // Release previously active header element.
4875                                 if ( activeHeader && activeHeader.element ) {
4876                                         releaseStickyHeader( activeHeader.element );
4877                                 }
4878
4879                                 if ( ! newInstance ) {
4880                                         if ( ! expandedSection && expandedPanel && expandedPanel.contentContainer ) {
4881                                                 newInstance = expandedPanel;
4882                                         } else if ( ! expandedPanel && expandedSection && expandedSection.contentContainer ) {
4883                                                 newInstance = expandedSection;
4884                                         } else {
4885                                                 activeHeader = false;
4886                                                 return;
4887                                         }
4888                                 }
4889
4890                                 headerElement = newInstance.contentContainer.find( '.customize-section-title, .panel-meta' ).first();
4891                                 if ( headerElement.length ) {
4892                                         activeHeader = {
4893                                                 instance: newInstance,
4894                                                 element:  headerElement,
4895                                                 parent:   headerElement.closest( '.customize-pane-child' ),
4896                                                 height:   getHeaderHeight( headerElement )
4897                                         };
4898                                         if ( expandedSection ) {
4899                                                 resetStickyHeader( activeHeader.element, activeHeader.parent );
4900                                         }
4901                                 } else {
4902                                         activeHeader = false;
4903                                 }
4904                         };
4905                         api.state( 'expandedSection' ).bind( changeContainer );
4906                         api.state( 'expandedPanel' ).bind( changeContainer );
4907
4908                         // Throttled scroll event handler.
4909                         parentContainer.on( 'scroll', _.throttle( function() {
4910                                 if ( ! activeHeader ) {
4911                                         return;
4912                                 }
4913
4914                                 var scrollTop = parentContainer.scrollTop(),
4915                                         isScrollingUp = ( lastScrollTop ) ? scrollTop <= lastScrollTop : true;
4916
4917                                 lastScrollTop = scrollTop;
4918                                 positionStickyHeader( activeHeader, scrollTop, isScrollingUp );
4919                         }, 8 ) );
4920
4921                         // Release header element if it is sticky.
4922                         releaseStickyHeader = function( headerElement ) {
4923                                 if ( ! headerElement.hasClass( 'is-sticky' ) ) {
4924                                         return;
4925                                 }
4926                                 headerElement
4927                                         .removeClass( 'is-sticky' )
4928                                         .addClass( 'maybe-sticky is-in-view' )
4929                                         .css( 'top', parentContainer.scrollTop() + 'px' );
4930                         };
4931
4932                         // Reset position of the sticky header.
4933                         resetStickyHeader = function( headerElement, headerParent ) {
4934                                 headerElement
4935                                         .removeClass( 'maybe-sticky is-in-view' )
4936                                         .css( {
4937                                                 width: '',
4938                                                 top: ''
4939                                         } );
4940                                 headerParent.css( 'padding-top', '' );
4941                         };
4942
4943                         /**
4944                          * Get header height.
4945                          *
4946                          * @since 4.7.0
4947                          * @access private
4948                          *
4949                          * @param {jQuery} headerElement Header element.
4950                          * @returns {number} Height.
4951                          */
4952                         getHeaderHeight = function( headerElement ) {
4953                                 var height = headerElement.data( 'height' );
4954                                 if ( ! height ) {
4955                                         height = headerElement.outerHeight();
4956                                         headerElement.data( 'height', height );
4957                                 }
4958                                 return height;
4959                         };
4960
4961                         /**
4962                          * Reposition header on throttled `scroll` event.
4963                          *
4964                          * @since 4.7.0
4965                          * @access private
4966                          *
4967                          * @param {object}  header        Header.
4968                          * @param {number}  scrollTop     Scroll top.
4969                          * @param {boolean} isScrollingUp Is scrolling up?
4970                          * @returns {void}
4971                          */
4972                         positionStickyHeader = function( header, scrollTop, isScrollingUp ) {
4973                                 var headerElement = header.element,
4974                                         headerParent = header.parent,
4975                                         headerHeight = header.height,
4976                                         headerTop = parseInt( headerElement.css( 'top' ), 10 ),
4977                                         maybeSticky = headerElement.hasClass( 'maybe-sticky' ),
4978                                         isSticky = headerElement.hasClass( 'is-sticky' ),
4979                                         isInView = headerElement.hasClass( 'is-in-view' );
4980
4981                                 // When scrolling down, gradually hide sticky header.
4982                                 if ( ! isScrollingUp ) {
4983                                         if ( isSticky ) {
4984                                                 headerTop = scrollTop;
4985                                                 headerElement
4986                                                         .removeClass( 'is-sticky' )
4987                                                         .css( {
4988                                                                 top:   headerTop + 'px',
4989                                                                 width: ''
4990                                                         } );
4991                                         }
4992                                         if ( isInView && scrollTop > headerTop + headerHeight ) {
4993                                                 headerElement.removeClass( 'is-in-view' );
4994                                                 headerParent.css( 'padding-top', '' );
4995                                         }
4996                                         return;
4997                                 }
4998
4999                                 // Scrolling up.
5000                                 if ( ! maybeSticky && scrollTop >= headerHeight ) {
5001                                         maybeSticky = true;
5002                                         headerElement.addClass( 'maybe-sticky' );
5003                                 } else if ( 0 === scrollTop ) {
5004                                         // Reset header in base position.
5005                                         headerElement
5006                                                 .removeClass( 'maybe-sticky is-in-view is-sticky' )
5007                                                 .css( {
5008                                                         top:   '',
5009                                                         width: ''
5010                                                 } );
5011                                         headerParent.css( 'padding-top', '' );
5012                                         return;
5013                                 }
5014
5015                                 if ( isInView && ! isSticky ) {
5016                                         // Header is in the view but is not yet sticky.
5017                                         if ( headerTop >= scrollTop ) {
5018                                                 // Header is fully visible.
5019                                                 headerElement
5020                                                         .addClass( 'is-sticky' )
5021                                                         .css( {
5022                                                                 top:   '',
5023                                                                 width: headerParent.outerWidth() + 'px'
5024                                                         } );
5025                                         }
5026                                 } else if ( maybeSticky && ! isInView ) {
5027                                         // Header is out of the view.
5028                                         headerElement
5029                                                 .addClass( 'is-in-view' )
5030                                                 .css( 'top', ( scrollTop - headerHeight ) + 'px' );
5031                                         headerParent.css( 'padding-top', headerHeight + 'px' );
5032                                 }
5033                         };
5034                 }());
5035
5036                 // Previewed device bindings.
5037                 api.previewedDevice = new api.Value();
5038
5039                 // Set the default device.
5040                 api.bind( 'ready', function() {
5041                         _.find( api.settings.previewableDevices, function( value, key ) {
5042                                 if ( true === value['default'] ) {
5043                                         api.previewedDevice.set( key );
5044                                         return true;
5045                                 }
5046                         } );
5047                 } );
5048
5049                 // Set the toggled device.
5050                 footerActions.find( '.devices button' ).on( 'click', function( event ) {
5051                         api.previewedDevice.set( $( event.currentTarget ).data( 'device' ) );
5052                 });
5053
5054                 // Bind device changes.
5055                 api.previewedDevice.bind( function( newDevice ) {
5056                         var overlay = $( '.wp-full-overlay' ),
5057                                 devices = '';
5058
5059                         footerActions.find( '.devices button' )
5060                                 .removeClass( 'active' )
5061                                 .attr( 'aria-pressed', false );
5062
5063                         footerActions.find( '.devices .preview-' + newDevice )
5064                                 .addClass( 'active' )
5065                                 .attr( 'aria-pressed', true );
5066
5067                         $.each( api.settings.previewableDevices, function( device ) {
5068                                 devices += ' preview-' + device;
5069                         } );
5070
5071                         overlay
5072                                 .removeClass( devices )
5073                                 .addClass( 'preview-' + newDevice );
5074                 } );
5075
5076                 // Bind site title display to the corresponding field.
5077                 if ( title.length ) {
5078                         api( 'blogname', function( setting ) {
5079                                 var updateTitle = function() {
5080                                         title.text( $.trim( setting() ) || api.l10n.untitledBlogName );
5081                                 };
5082                                 setting.bind( updateTitle );
5083                                 updateTitle();
5084                         } );
5085                 }
5086
5087                 /*
5088                  * Create a postMessage connection with a parent frame,
5089                  * in case the Customizer frame was opened with the Customize loader.
5090                  *
5091                  * @see wp.customize.Loader
5092                  */
5093                 parent = new api.Messenger({
5094                         url: api.settings.url.parent,
5095                         channel: 'loader'
5096                 });
5097
5098                 /*
5099                  * If we receive a 'back' event, we're inside an iframe.
5100                  * Send any clicks to the 'Return' link to the parent page.
5101                  */
5102                 parent.bind( 'back', function() {
5103                         closeBtn.on( 'click.customize-controls-close', function( event ) {
5104                                 event.preventDefault();
5105                                 parent.send( 'close' );
5106                         });
5107                 });
5108
5109                 // Prompt user with AYS dialog if leaving the Customizer with unsaved changes
5110                 $( window ).on( 'beforeunload.customize-confirm', function () {
5111                         if ( ! api.state( 'saved' )() ) {
5112                                 setTimeout( function() {
5113                                         overlay.removeClass( 'customize-loading' );
5114                                 }, 1 );
5115                                 return api.l10n.saveAlert;
5116                         }
5117                 } );
5118
5119                 // Pass events through to the parent.
5120                 $.each( [ 'saved', 'change' ], function ( i, event ) {
5121                         api.bind( event, function() {
5122                                 parent.send( event );
5123                         });
5124                 } );
5125
5126                 // Pass titles to the parent
5127                 api.bind( 'title', function( newTitle ) {
5128                         parent.send( 'title', newTitle );
5129                 });
5130
5131                 parent.send( 'changeset-uuid', api.settings.changeset.uuid );
5132
5133                 // Initialize the connection with the parent frame.
5134                 parent.send( 'ready' );
5135
5136                 // Control visibility for default controls
5137                 $.each({
5138                         'background_image': {
5139                                 controls: [ 'background_preset', 'background_position', 'background_size', 'background_repeat', 'background_attachment' ],
5140                                 callback: function( to ) { return !! to; }
5141                         },
5142                         'show_on_front': {
5143                                 controls: [ 'page_on_front', 'page_for_posts' ],
5144                                 callback: function( to ) { return 'page' === to; }
5145                         },
5146                         'header_textcolor': {
5147                                 controls: [ 'header_textcolor' ],
5148                                 callback: function( to ) { return 'blank' !== to; }
5149                         }
5150                 }, function( settingId, o ) {
5151                         api( settingId, function( setting ) {
5152                                 $.each( o.controls, function( i, controlId ) {
5153                                         api.control( controlId, function( control ) {
5154                                                 var visibility = function( to ) {
5155                                                         control.container.toggle( o.callback( to ) );
5156                                                 };
5157
5158                                                 visibility( setting.get() );
5159                                                 setting.bind( visibility );
5160                                         });
5161                                 });
5162                         });
5163                 });
5164
5165                 api.control( 'background_preset', function( control ) {
5166                         var visibility, defaultValues, values, toggleVisibility, updateSettings, preset;
5167
5168                         visibility = { // position, size, repeat, attachment
5169                                 'default': [ false, false, false, false ],
5170                                 'fill': [ true, false, false, false ],
5171                                 'fit': [ true, false, true, false ],
5172                                 'repeat': [ true, false, false, true ],
5173                                 'custom': [ true, true, true, true ]
5174                         };
5175
5176                         defaultValues = [
5177                                 _wpCustomizeBackground.defaults['default-position-x'],
5178                                 _wpCustomizeBackground.defaults['default-position-y'],
5179                                 _wpCustomizeBackground.defaults['default-size'],
5180                                 _wpCustomizeBackground.defaults['default-repeat'],
5181                                 _wpCustomizeBackground.defaults['default-attachment']
5182                         ];
5183
5184                         values = { // position_x, position_y, size, repeat, attachment
5185                                 'default': defaultValues,
5186                                 'fill': [ 'left', 'top', 'cover', 'no-repeat', 'fixed' ],
5187                                 'fit': [ 'left', 'top', 'contain', 'no-repeat', 'fixed' ],
5188                                 'repeat': [ 'left', 'top', 'auto', 'repeat', 'scroll' ]
5189                         };
5190
5191                         // @todo These should actually toggle the active state, but without the preview overriding the state in data.activeControls.
5192                         toggleVisibility = function( preset ) {
5193                                 _.each( [ 'background_position', 'background_size', 'background_repeat', 'background_attachment' ], function( controlId, i ) {
5194                                         var control = api.control( controlId );
5195                                         if ( control ) {
5196                                                 control.container.toggle( visibility[ preset ][ i ] );
5197                                         }
5198                                 } );
5199                         };
5200
5201                         updateSettings = function( preset ) {
5202                                 _.each( [ 'background_position_x', 'background_position_y', 'background_size', 'background_repeat', 'background_attachment' ], function( settingId, i ) {
5203                                         var setting = api( settingId );
5204                                         if ( setting ) {
5205                                                 setting.set( values[ preset ][ i ] );
5206                                         }
5207                                 } );
5208                         };
5209
5210                         preset = control.setting.get();
5211                         toggleVisibility( preset );
5212
5213                         control.setting.bind( 'change', function( preset ) {
5214                                 toggleVisibility( preset );
5215                                 if ( 'custom' !== preset ) {
5216                                         updateSettings( preset );
5217                                 }
5218                         } );
5219                 } );
5220
5221                 api.control( 'background_repeat', function( control ) {
5222                         control.elements[0].unsync( api( 'background_repeat' ) );
5223
5224                         control.element = new api.Element( control.container.find( 'input' ) );
5225                         control.element.set( 'no-repeat' !== control.setting() );
5226
5227                         control.element.bind( function( to ) {
5228                                 control.setting.set( to ? 'repeat' : 'no-repeat' );
5229                         } );
5230
5231                         control.setting.bind( function( to ) {
5232                                 control.element.set( 'no-repeat' !== to );
5233                         } );
5234                 } );
5235
5236                 api.control( 'background_attachment', function( control ) {
5237                         control.elements[0].unsync( api( 'background_attachment' ) );
5238
5239                         control.element = new api.Element( control.container.find( 'input' ) );
5240                         control.element.set( 'fixed' !== control.setting() );
5241
5242                         control.element.bind( function( to ) {
5243                                 control.setting.set( to ? 'scroll' : 'fixed' );
5244                         } );
5245
5246                         control.setting.bind( function( to ) {
5247                                 control.element.set( 'fixed' !== to );
5248                         } );
5249                 } );
5250
5251                 // Juggle the two controls that use header_textcolor
5252                 api.control( 'display_header_text', function( control ) {
5253                         var last = '';
5254
5255                         control.elements[0].unsync( api( 'header_textcolor' ) );
5256
5257                         control.element = new api.Element( control.container.find('input') );
5258                         control.element.set( 'blank' !== control.setting() );
5259
5260                         control.element.bind( function( to ) {
5261                                 if ( ! to )
5262                                         last = api( 'header_textcolor' ).get();
5263
5264                                 control.setting.set( to ? last : 'blank' );
5265                         });
5266
5267                         control.setting.bind( function( to ) {
5268                                 control.element.set( 'blank' !== to );
5269                         });
5270                 });
5271
5272                 // Change previewed URL to the homepage when changing the page_on_front.
5273                 api( 'show_on_front', 'page_on_front', function( showOnFront, pageOnFront ) {
5274                         var updatePreviewUrl = function() {
5275                                 if ( showOnFront() === 'page' && parseInt( pageOnFront(), 10 ) > 0 ) {
5276                                         api.previewer.previewUrl.set( api.settings.url.home );
5277                                 }
5278                         };
5279                         showOnFront.bind( updatePreviewUrl );
5280                         pageOnFront.bind( updatePreviewUrl );
5281                 });
5282
5283                 // Change the previewed URL to the selected page when changing the page_for_posts.
5284                 api( 'page_for_posts', function( setting ) {
5285                         setting.bind(function( pageId ) {
5286                                 pageId = parseInt( pageId, 10 );
5287                                 if ( pageId > 0 ) {
5288                                         api.previewer.previewUrl.set( api.settings.url.home + '?page_id=' + pageId );
5289                                 }
5290                         });
5291                 });
5292
5293                 // Allow tabs to be entered in Custom CSS textarea.
5294                 api.control( 'custom_css', function setupCustomCssControl( control ) {
5295                         control.deferred.embedded.done( function allowTabs() {
5296                                 var $textarea = control.container.find( 'textarea' ), textarea = $textarea[0];
5297
5298                                 $textarea.on( 'blur', function onBlur() {
5299                                         $textarea.data( 'next-tab-blurs', false );
5300                                 } );
5301
5302                                 $textarea.on( 'keydown', function onKeydown( event ) {
5303                                         var selectionStart, selectionEnd, value, tabKeyCode = 9, escKeyCode = 27;
5304
5305                                         if ( escKeyCode === event.keyCode ) {
5306                                                 if ( ! $textarea.data( 'next-tab-blurs' ) ) {
5307                                                         $textarea.data( 'next-tab-blurs', true );
5308                                                         event.stopPropagation(); // Prevent collapsing the section.
5309                                                 }
5310                                                 return;
5311                                         }
5312
5313                                         // Short-circuit if tab key is not being pressed or if a modifier key *is* being pressed.
5314                                         if ( tabKeyCode !== event.keyCode || event.ctrlKey || event.altKey || event.shiftKey ) {
5315                                                 return;
5316                                         }
5317
5318                                         // Prevent capturing Tab characters if Esc was pressed.
5319                                         if ( $textarea.data( 'next-tab-blurs' ) ) {
5320                                                 return;
5321                                         }
5322
5323                                         selectionStart = textarea.selectionStart;
5324                                         selectionEnd = textarea.selectionEnd;
5325                                         value = textarea.value;
5326
5327                                         if ( selectionStart >= 0 ) {
5328                                                 textarea.value = value.substring( 0, selectionStart ).concat( '\t', value.substring( selectionEnd ) );
5329                                                 $textarea.selectionStart = textarea.selectionEnd = selectionStart + 1;
5330                                         }
5331
5332                                         event.stopPropagation();
5333                                         event.preventDefault();
5334                                 } );
5335                         } );
5336                 } );
5337
5338                 // Toggle visibility of Header Video notice when active state change.
5339                 api.control( 'header_video', function( headerVideoControl ) {
5340                         headerVideoControl.deferred.embedded.done( function() {
5341                                 var toggleNotice = function() {
5342                                         var section = api.section( headerVideoControl.section() ), notice;
5343                                         if ( ! section ) {
5344                                                 return;
5345                                         }
5346                                         notice = section.container.find( '.header-video-not-currently-previewable:first' );
5347                                         if ( headerVideoControl.active.get() ) {
5348                                                 notice.stop().slideUp( 'fast' );
5349                                         } else {
5350                                                 notice.stop().slideDown( 'fast' );
5351                                         }
5352                                 };
5353                                 toggleNotice();
5354                                 headerVideoControl.active.bind( toggleNotice );
5355                         } );
5356                 } );
5357
5358                 // Update the setting validities.
5359                 api.previewer.bind( 'selective-refresh-setting-validities', function handleSelectiveRefreshedSettingValidities( settingValidities ) {
5360                         api._handleSettingValidities( {
5361                                 settingValidities: settingValidities,
5362                                 focusInvalidControl: false
5363                         } );
5364                 } );
5365
5366                 // Focus on the control that is associated with the given setting.
5367                 api.previewer.bind( 'focus-control-for-setting', function( settingId ) {
5368                         var matchedControls = [];
5369                         api.control.each( function( control ) {
5370                                 var settingIds = _.pluck( control.settings, 'id' );
5371                                 if ( -1 !== _.indexOf( settingIds, settingId ) ) {
5372                                         matchedControls.push( control );
5373                                 }
5374                         } );
5375
5376                         // Focus on the matched control with the lowest priority (appearing higher).
5377                         if ( matchedControls.length ) {
5378                                 matchedControls.sort( function( a, b ) {
5379                                         return a.priority() - b.priority();
5380                                 } );
5381                                 matchedControls[0].focus();
5382                         }
5383                 } );
5384
5385                 // Refresh the preview when it requests.
5386                 api.previewer.bind( 'refresh', function() {
5387                         api.previewer.refresh();
5388                 });
5389
5390                 // Update the edit shortcut visibility state.
5391                 api.state( 'paneVisible' ).bind( function( isPaneVisible ) {
5392                         var isMobileScreen;
5393                         if ( window.matchMedia ) {
5394                                 isMobileScreen = window.matchMedia( 'screen and ( max-width: 640px )' ).matches;
5395                         } else {
5396                                 isMobileScreen = $( window ).width() <= 640;
5397                         }
5398                         api.state( 'editShortcutVisibility' ).set( isPaneVisible || isMobileScreen ? 'visible' : 'hidden' );
5399                 } );
5400                 if ( window.matchMedia ) {
5401                         window.matchMedia( 'screen and ( max-width: 640px )' ).addListener( function() {
5402                                 var state = api.state( 'paneVisible' );
5403                                 state.callbacks.fireWith( state, [ state.get(), state.get() ] );
5404                         } );
5405                 }
5406                 api.previewer.bind( 'edit-shortcut-visibility', function( visibility ) {
5407                         api.state( 'editShortcutVisibility' ).set( visibility );
5408                 } );
5409                 api.state( 'editShortcutVisibility' ).bind( function( visibility ) {
5410                         api.previewer.send( 'edit-shortcut-visibility', visibility );
5411                 } );
5412
5413                 // Autosave changeset.
5414                 ( function() {
5415                         var timeoutId, updateChangesetWithReschedule, scheduleChangesetUpdate, updatePending = false;
5416
5417                         /**
5418                          * Request changeset update and then re-schedule the next changeset update time.
5419                          *
5420                          * @since 4.7.0
5421                          * @private
5422                          */
5423                         updateChangesetWithReschedule = function() {
5424                                 if ( ! updatePending ) {
5425                                         updatePending = true;
5426                                         api.requestChangesetUpdate().always( function() {
5427                                                 updatePending = false;
5428                                         } );
5429                                 }
5430                                 scheduleChangesetUpdate();
5431                         };
5432
5433                         /**
5434                          * Schedule changeset update.
5435                          *
5436                          * @since 4.7.0
5437                          * @private
5438                          */
5439                         scheduleChangesetUpdate = function() {
5440                                 clearTimeout( timeoutId );
5441                                 timeoutId = setTimeout( function() {
5442                                         updateChangesetWithReschedule();
5443                                 }, api.settings.timeouts.changesetAutoSave );
5444                         };
5445
5446                         // Start auto-save interval for updating changeset.
5447                         scheduleChangesetUpdate();
5448
5449                         // Save changeset when focus removed from window.
5450                         $( window ).on( 'blur.wp-customize-changeset-update', function() {
5451                                 updateChangesetWithReschedule();
5452                         } );
5453
5454                         // Save changeset before unloading window.
5455                         $( window ).on( 'beforeunload.wp-customize-changeset-update', function() {
5456                                 updateChangesetWithReschedule();
5457                         } );
5458                 } ());
5459
5460                 api.trigger( 'ready' );
5461         });
5462
5463 })( wp, jQuery );