]> scripts.mit.edu Git - autoinstalls/wordpress.git/blob - wp-admin/js/customize-controls.js
65f5584e8a91f0256a3529616783e27041186118
[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         &n