]> scripts.mit.edu Git - autoinstalls/wordpress.git/blob - wp-admin/js/customize-controls.js
Wordpress 4.6
[autoinstalls/wordpress.git] / wp-admin / js / customize-controls.js
1 /* global _wpCustomizeHeader, _wpCustomizeBackground, _wpMediaViewsL10n, MediaElementPlayer */
2 (function( exports, $ ){
3         var Container, focus, 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                         api.Value.prototype.initialize.call( this, value, options );
26
27                         this.id = id;
28                         this.transport = this.transport || 'refresh';
29                         this._dirty = options.dirty || false;
30                         this.notifications = new api.Values({ defaultConstructor: api.Notification });
31
32                         // Whenever the setting's value changes, refresh the preview.
33                         this.bind( this.preview );
34                 },
35
36                 /**
37                  * Refresh the preview, respective of the setting's refresh policy.
38                  */
39                 preview: function() {
40                         switch ( this.transport ) {
41                                 case 'refresh':
42                                         return this.previewer.refresh();
43                                 case 'postMessage':
44                                         return this.previewer.send( 'setting', [ this.id, this() ] );
45                         }
46                 },
47
48                 /**
49                  * Find controls associated with this setting.
50                  *
51                  * @since 4.6.0
52                  * @returns {wp.customize.Control[]} Controls associated with setting.
53                  */
54                 findControls: function() {
55                         var setting = this, controls = [];
56                         api.control.each( function( control ) {
57                                 _.each( control.settings, function( controlSetting ) {
58                                         if ( controlSetting.id === setting.id ) {
59                                                 controls.push( control );
60                                         }
61                                 } );
62                         } );
63                         return controls;
64                 }
65         });
66
67         /**
68          * Utility function namespace
69          */
70         api.utils = {};
71
72         /**
73          * Watch all changes to Value properties, and bubble changes to parent Values instance
74          *
75          * @since 4.1.0
76          *
77          * @param {wp.customize.Class} instance
78          * @param {Array}              properties  The names of the Value instances to watch.
79          */
80         api.utils.bubbleChildValueChanges = function ( instance, properties ) {
81                 $.each( properties, function ( i, key ) {
82                         instance[ key ].bind( function ( to, from ) {
83                                 if ( instance.parent && to !== from ) {
84                                         instance.parent.trigger( 'change', instance );
85                                 }
86                         } );
87                 } );
88         };
89
90         /**
91          * Expand a panel, section, or control and focus on the first focusable element.
92          *
93          * @since 4.1.0
94          *
95          * @param {Object}   [params]
96          * @param {Function} [params.completeCallback]
97          */
98         focus = function ( params ) {
99                 var construct, completeCallback, focus, focusElement;
100                 construct = this;
101                 params = params || {};
102                 focus = function () {
103                         var focusContainer;
104                         if ( construct.extended( api.Panel ) && construct.expanded && construct.expanded() ) {
105                                 focusContainer = construct.container.find( 'ul.control-panel-content' );
106                         } else if ( construct.extended( api.Section ) && construct.expanded && construct.expanded() ) {
107                                 focusContainer = construct.container.find( 'ul.accordion-section-content' );
108                         } else {
109                                 focusContainer = construct.container;
110                         }
111
112                         focusElement = focusContainer.find( '.control-focus:first' );
113                         if ( 0 === focusElement.length ) {
114                                 // Note that we can't use :focusable due to a jQuery UI issue. See: https://github.com/jquery/jquery-ui/pull/1583
115                                 focusElement = focusContainer.find( 'input, select, textarea, button, object, a[href], [tabindex]' ).filter( ':visible' ).first();
116                         }
117                         focusElement.focus();
118                 };
119                 if ( params.completeCallback ) {
120                         completeCallback = params.completeCallback;
121                         params.completeCallback = function () {
122                                 focus();
123                                 completeCallback();
124                         };
125                 } else {
126                         params.completeCallback = focus;
127                 }
128                 if ( construct.expand ) {
129                         construct.expand( params );
130                 } else {
131                         params.completeCallback();
132                 }
133         };
134
135         /**
136          * Stable sort for Panels, Sections, and Controls.
137          *
138          * If a.priority() === b.priority(), then sort by their respective params.instanceNumber.
139          *
140          * @since 4.1.0
141          *
142          * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} a
143          * @param {(wp.customize.Panel|wp.customize.Section|wp.customize.Control)} b
144          * @returns {Number}
145          */
146         api.utils.prioritySort = function ( a, b ) {
147                 if ( a.priority() === b.priority() && typeof a.params.instanceNumber === 'number' && typeof b.params.instanceNumber === 'number' ) {
148                         return a.params.instanceNumber - b.params.instanceNumber;
149                 } else {
150                         return a.priority() - b.priority();
151                 }
152         };
153
154         /**
155          * Return whether the supplied Event object is for a keydown event but not the Enter key.
156          *
157          * @since 4.1.0
158          *
159          * @param {jQuery.Event} event
160          * @returns {boolean}
161          */
162         api.utils.isKeydownButNotEnterEvent = function ( event ) {
163                 return ( 'keydown' === event.type && 13 !== event.which );
164         };
165
166         /**
167          * Return whether the two lists of elements are the same and are in the same order.
168          *
169          * @since 4.1.0
170          *
171          * @param {Array|jQuery} listA
172          * @param {Array|jQuery} listB
173          * @returns {boolean}
174          */
175         api.utils.areElementListsEqual = function ( listA, listB ) {
176                 var equal = (
177                         listA.length === listB.length && // if lists are different lengths, then naturally they are not equal
178                         -1 === _.indexOf( _.map( // are there any false values in the list returned by map?
179                                 _.zip( listA, listB ), // pair up each element between the two lists
180                                 function ( pair ) {
181                                         return $( pair[0] ).is( pair[1] ); // compare to see if each pair are equal
182                                 }
183                         ), false ) // check for presence of false in map's return value
184                 );
185                 return equal;
186         };
187
188         /**
189          * Base class for Panel and Section.
190          *
191          * @since 4.1.0
192          *
193          * @class
194          * @augments wp.customize.Class
195          */
196         Container = api.Class.extend({
197                 defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
198                 defaultExpandedArguments: { duration: 'fast', completeCallback: $.noop },
199                 containerType: 'container',
200                 defaults: {
201                         title: '',
202                         description: '',
203                         priority: 100,
204                         type: 'default',
205                         content: null,
206                         active: true,
207                         instanceNumber: null
208                 },
209
210                 /**
211                  * @since 4.1.0
212                  *
213                  * @param {string}         id - The ID for the container.
214                  * @param {object}         options - Object containing one property: params.
215                  * @param {object}         options.params - Object containing the following properties.
216                  * @param {string}         options.params.title - Title shown when panel is collapsed and expanded.
217                  * @param {string=}        [options.params.description] - Description shown at the top of the panel.
218                  * @param {number=100}     [options.params.priority] - The sort priority for the panel.
219                  * @param {string=default} [options.params.type] - The type of the panel. See wp.customize.panelConstructor.
220                  * @param {string=}        [options.params.content] - The markup to be used for the panel container. If empty, a JS template is used.
221                  * @param {boolean=true}   [options.params.active] - Whether the panel is active or not.
222                  */
223                 initialize: function ( id, options ) {
224                         var container = this;
225                         container.id = id;
226                         options = options || {};
227
228                         options.params = _.defaults(
229                                 options.params || {},
230                                 container.defaults
231                         );
232
233                         $.extend( container, options );
234                         container.templateSelector = 'customize-' + container.containerType + '-' + container.params.type;
235                         container.container = $( container.params.content );
236                         if ( 0 === container.container.length ) {
237                                 container.container = $( container.getContainer() );
238                         }
239
240                         container.deferred = {
241                                 embedded: new $.Deferred()
242                         };
243                         container.priority = new api.Value();
244                         container.active = new api.Value();
245                         container.activeArgumentsQueue = [];
246                         container.expanded = new api.Value();
247                         container.expandedArgumentsQueue = [];
248
249                         container.active.bind( function ( active ) {
250                                 var args = container.activeArgumentsQueue.shift();
251                                 args = $.extend( {}, container.defaultActiveArguments, args );
252                                 active = ( active && container.isContextuallyActive() );
253                                 container.onChangeActive( active, args );
254                         });
255                         container.expanded.bind( function ( expanded ) {
256                                 var args = container.expandedArgumentsQueue.shift();
257                                 args = $.extend( {}, container.defaultExpandedArguments, args );
258                                 container.onChangeExpanded( expanded, args );
259                         });
260
261                         container.deferred.embedded.done( function () {
262                                 container.attachEvents();
263                         });
264
265                         api.utils.bubbleChildValueChanges( container, [ 'priority', 'active' ] );
266
267                         container.priority.set( container.params.priority );
268                         container.active.set( container.params.active );
269                         container.expanded.set( false );
270                 },
271
272                 /**
273                  * @since 4.1.0
274                  *
275                  * @abstract
276                  */
277                 ready: function() {},
278
279                 /**
280                  * Get the child models associated with this parent, sorting them by their priority Value.
281                  *
282                  * @since 4.1.0
283                  *
284                  * @param {String} parentType
285                  * @param {String} childType
286                  * @returns {Array}
287                  */
288                 _children: function ( parentType, childType ) {
289                         var parent = this,
290                                 children = [];
291                         api[ childType ].each( function ( child ) {
292                                 if ( child[ parentType ].get() === parent.id ) {
293                                         children.push( child );
294                                 }
295                         } );
296                         children.sort( api.utils.prioritySort );
297                         return children;
298                 },
299
300                 /**
301                  * To override by subclass, to return whether the container has active children.
302                  *
303                  * @since 4.1.0
304                  *
305                  * @abstract
306                  */
307                 isContextuallyActive: function () {
308                         throw new Error( 'Container.isContextuallyActive() must be overridden in a subclass.' );
309                 },
310
311                 /**
312                  * Active state change handler.
313                  *
314                  * Shows the container if it is active, hides it if not.
315                  *
316                  * To override by subclass, update the container's UI to reflect the provided active state.
317                  *
318                  * @since 4.1.0
319                  *
320                  * @param {Boolean} active
321                  * @param {Object}  args
322                  * @param {Object}  args.duration
323                  * @param {Object}  args.completeCallback
324                  */
325                 onChangeActive: function( active, args ) {
326                         var duration, construct = this, expandedOtherPanel;
327                         if ( args.unchanged ) {
328                                 if ( args.completeCallback ) {
329                                         args.completeCallback();
330                                 }
331                                 return;
332                         }
333
334                         duration = ( 'resolved' === api.previewer.deferred.active.state() ? args.duration : 0 );
335
336                         if ( construct.extended( api.Panel ) ) {
337                                 // If this is a panel is not currently expanded but another panel is expanded, do not animate.
338                                 api.panel.each(function ( panel ) {
339                                         if ( panel !== construct && panel.expanded() ) {
340                                                 expandedOtherPanel = panel;
341                                                 duration = 0;
342                                         }
343                                 });
344
345                                 // Collapse any expanded sections inside of this panel first before deactivating.
346                                 if ( ! active ) {
347                                         _.each( construct.sections(), function( section ) {
348                                                 section.collapse( { duration: 0 } );
349                                         } );
350                                 }
351                         }
352
353                         if ( ! $.contains( document, construct.container[0] ) ) {
354                                 // jQuery.fn.slideUp is not hiding an element if it is not in the DOM
355                                 construct.container.toggle( active );
356                                 if ( args.completeCallback ) {
357                                         args.completeCallback();
358                                 }
359                         } else if ( active ) {
360                                 construct.container.stop( true, true ).slideDown( duration, args.completeCallback );
361                         } else {
362                                 if ( construct.expanded() ) {
363                                         construct.collapse({
364                                                 duration: duration,
365                                                 completeCallback: function() {
366                                                         construct.container.stop( true, true ).slideUp( duration, args.completeCallback );
367                                                 }
368                                         });
369                                 } else {
370                                         construct.container.stop( true, true ).slideUp( duration, args.completeCallback );
371                                 }
372                         }
373
374                         // Recalculate the margin-top immediately, not waiting for debounced reflow, to prevent momentary (100ms) vertical jiggle.
375                         if ( expandedOtherPanel ) {
376                                 expandedOtherPanel._recalculateTopMargin();
377                         }
378                 },
379
380                 /**
381                  * @since 4.1.0
382                  *
383                  * @params {Boolean} active
384                  * @param {Object}   [params]
385                  * @returns {Boolean} false if state already applied
386                  */
387                 _toggleActive: function ( active, params ) {
388                         var self = this;
389                         params = params || {};
390                         if ( ( active && this.active.get() ) || ( ! active && ! this.active.get() ) ) {
391                                 params.unchanged = true;
392                                 self.onChangeActive( self.active.get(), params );
393                                 return false;
394                         } else {
395                                 params.unchanged = false;
396                                 this.activeArgumentsQueue.push( params );
397                                 this.active.set( active );
398                                 return true;
399                         }
400                 },
401
402                 /**
403                  * @param {Object} [params]
404                  * @returns {Boolean} false if already active
405                  */
406                 activate: function ( params ) {
407                         return this._toggleActive( true, params );
408                 },
409
410                 /**
411                  * @param {Object} [params]
412                  * @returns {Boolean} false if already inactive
413                  */
414                 deactivate: function ( params ) {
415                         return this._toggleActive( false, params );
416                 },
417
418                 /**
419                  * To override by subclass, update the container's UI to reflect the provided active state.
420                  * @abstract
421                  */
422                 onChangeExpanded: function () {
423                         throw new Error( 'Must override with subclass.' );
424                 },
425
426                 /**
427                  * Handle the toggle logic for expand/collapse.
428                  *
429                  * @param {Boolean}  expanded - The new state to apply.
430                  * @param {Object}   [params] - Object containing options for expand/collapse.
431                  * @param {Function} [params.completeCallback] - Function to call when expansion/collapse is complete.
432                  * @returns {Boolean} false if state already applied or active state is false
433                  */
434                 _toggleExpanded: function( expanded, params ) {
435                         var instance = this, previousCompleteCallback;
436                         params = params || {};
437                         previousCompleteCallback = params.completeCallback;
438
439                         // Short-circuit expand() if the instance is not active.
440                         if ( expanded && ! instance.active() ) {
441                                 return false;
442                         }
443
444                         params.completeCallback = function() {
445                                 if ( previousCompleteCallback ) {
446                                         previousCompleteCallback.apply( instance, arguments );
447                                 }
448                                 if ( expanded ) {
449                                         instance.container.trigger( 'expanded' );
450                                 } else {
451                                         instance.container.trigger( 'collapsed' );
452                                 }
453                         };
454                         if ( ( expanded && instance.expanded.get() ) || ( ! expanded && ! instance.expanded.get() ) ) {
455                                 params.unchanged = true;
456                                 instance.onChangeExpanded( instance.expanded.get(), params );
457                                 return false;
458                         } else {
459                                 params.unchanged = false;
460                                 instance.expandedArgumentsQueue.push( params );
461                                 instance.expanded.set( expanded );
462                                 return true;
463                         }
464                 },
465
466                 /**
467                  * @param {Object} [params]
468                  * @returns {Boolean} false if already expanded or if inactive.
469                  */
470                 expand: function ( params ) {
471                         return this._toggleExpanded( true, params );
472                 },
473
474                 /**
475                  * @param {Object} [params]
476                  * @returns {Boolean} false if already collapsed.
477                  */
478                 collapse: function ( params ) {
479                         return this._toggleExpanded( false, params );
480                 },
481
482                 /**
483                  * Bring the container into view and then expand this and bring it into view
484                  * @param {Object} [params]
485                  */
486                 focus: focus,
487
488                 /**
489                  * Return the container html, generated from its JS template, if it exists.
490                  *
491                  * @since 4.3.0
492                  */
493                 getContainer: function () {
494                         var template,
495                                 container = this;
496
497                         if ( 0 !== $( '#tmpl-' + container.templateSelector ).length ) {
498                                 template = wp.template( container.templateSelector );
499                         } else {
500                                 template = wp.template( 'customize-' + container.containerType + '-default' );
501                         }
502                         if ( template && container.container ) {
503                                 return $.trim( template( container.params ) );
504                         }
505
506                         return '<li></li>';
507                 }
508         });
509
510         /**
511          * @since 4.1.0
512          *
513          * @class
514          * @augments wp.customize.Class
515          */
516         api.Section = Container.extend({
517                 containerType: 'section',
518                 defaults: {
519                         title: '',
520                         description: '',
521                         priority: 100,
522                         type: 'default',
523                         content: null,
524                         active: true,
525                         instanceNumber: null,
526                         panel: null,
527                         customizeAction: ''
528                 },
529
530                 /**
531                  * @since 4.1.0
532                  *
533                  * @param {string}         id - The ID for the section.
534                  * @param {object}         options - Object containing one property: params.
535                  * @param {object}         options.params - Object containing the following properties.
536                  * @param {string}         options.params.title - Title shown when section is collapsed and expanded.
537                  * @param {string=}        [options.params.description] - Description shown at the top of the section.
538                  * @param {number=100}     [options.params.priority] - The sort priority for the section.
539                  * @param {string=default} [options.params.type] - The type of the section. See wp.customize.sectionConstructor.
540                  * @param {string=}        [options.params.content] - The markup to be used for the section container. If empty, a JS template is used.
541                  * @param {boolean=true}   [options.params.active] - Whether the section is active or not.
542                  * @param {string}         options.params.panel - The ID for the panel this section is associated with.
543                  * @param {string=}        [options.params.customizeAction] - Additional context information shown before the section title when expanded.
544                  */
545                 initialize: function ( id, options ) {
546                         var section = this;
547                         Container.prototype.initialize.call( section, id, options );
548
549                         section.id = id;
550                         section.panel = new api.Value();
551                         section.panel.bind( function ( id ) {
552                                 $( section.container ).toggleClass( 'control-subsection', !! id );
553                         });
554                         section.panel.set( section.params.panel || '' );
555                         api.utils.bubbleChildValueChanges( section, [ 'panel' ] );
556
557                         section.embed();
558                         section.deferred.embedded.done( function () {
559                                 section.ready();
560                         });
561                 },
562
563                 /**
564                  * Embed the container in the DOM when any parent panel is ready.
565                  *
566                  * @since 4.1.0
567                  */
568                 embed: function () {
569                         var section = this, inject;
570
571                         // Watch for changes to the panel state
572                         inject = function ( panelId ) {
573                                 var parentContainer;
574                                 if ( panelId ) {
575                                         // The panel has been supplied, so wait until the panel object is registered
576                                         api.panel( panelId, function ( panel ) {
577                                                 // The panel has been registered, wait for it to become ready/initialized
578                                                 panel.deferred.embedded.done( function () {
579                                                         parentContainer = panel.container.find( 'ul:first' );
580                                                         if ( ! section.container.parent().is( parentContainer ) ) {
581                                                                 parentContainer.append( section.container );
582                                                         }
583                                                         section.deferred.embedded.resolve();
584                                                 });
585                                         } );
586                                 } else {
587                                         // There is no panel, so embed the section in the root of the customizer
588                                         parentContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable
589                                         if ( ! section.container.parent().is( parentContainer ) ) {
590                                                 parentContainer.append( section.container );
591                                         }
592                                         section.deferred.embedded.resolve();
593                                 }
594                         };
595                         section.panel.bind( inject );
596                         inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one
597
598                         section.deferred.embedded.done(function() {
599                                 // Fix the top margin after reflow.
600                                 api.bind( 'pane-contents-reflowed', _.debounce( function() {
601                                         section._recalculateTopMargin();
602                                 }, 100 ) );
603                         });
604                 },
605
606                 /**
607                  * Add behaviors for the accordion section.
608                  *
609                  * @since 4.1.0
610                  */
611                 attachEvents: function () {
612                         var section = this;
613
614                         // Expand/Collapse accordion sections on click.
615                         section.container.find( '.accordion-section-title, .customize-section-back' ).on( 'click keydown', function( event ) {
616                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
617                                         return;
618                                 }
619                                 event.preventDefault(); // Keep this AFTER the key filter above
620
621                                 if ( section.expanded() ) {
622                                         section.collapse();
623                                 } else {
624                                         section.expand();
625                                 }
626                         });
627                 },
628
629                 /**
630                  * Return whether this section has any active controls.
631                  *
632                  * @since 4.1.0
633                  *
634                  * @returns {Boolean}
635                  */
636                 isContextuallyActive: function () {
637                         var section = this,
638                                 controls = section.controls(),
639                                 activeCount = 0;
640                         _( controls ).each( function ( control ) {
641                                 if ( control.active() ) {
642                                         activeCount += 1;
643                                 }
644                         } );
645                         return ( activeCount !== 0 );
646                 },
647
648                 /**
649                  * Get the controls that are associated with this section, sorted by their priority Value.
650                  *
651                  * @since 4.1.0
652                  *
653                  * @returns {Array}
654                  */
655                 controls: function () {
656                         return this._children( 'section', 'control' );
657                 },
658
659                 /**
660                  * Update UI to reflect expanded state.
661                  *
662                  * @since 4.1.0
663                  *
664                  * @param {Boolean} expanded
665                  * @param {Object}  args
666                  */
667                 onChangeExpanded: function ( expanded, args ) {
668                         var section = this,
669                                 container = section.container.closest( '.wp-full-overlay-sidebar-content' ),
670                                 content = section.container.find( '.accordion-section-content' ),
671                                 overlay = section.container.closest( '.wp-full-overlay' ),
672                                 backBtn = section.container.find( '.customize-section-back' ),
673                                 sectionTitle = section.container.find( '.accordion-section-title' ).first(),
674                                 headerActionsHeight = $( '#customize-header-actions' ).height(),
675                                 resizeContentHeight, expand, position, scroll;
676
677                         if ( expanded && ! section.container.hasClass( 'open' ) ) {
678
679                                 if ( args.unchanged ) {
680                                         expand = args.completeCallback;
681                                 } else {
682                                         container.scrollTop( 0 );
683                                         resizeContentHeight = function() {
684                                                 var matchMedia, offset;
685                                                 matchMedia = window.matchMedia || window.msMatchMedia;
686                                                 offset = 90; // 45px for customize header actions + 45px for footer actions.
687
688                                                 // No footer on small screens.
689                                                 if ( matchMedia && matchMedia( '(max-width: 640px)' ).matches ) {
690                                                         offset = 45;
691                                                 }
692                                                 content.css( 'height', ( window.innerHeight - offset ) );
693                                         };
694                                         expand = function() {
695                                                 section.container.addClass( 'open' );
696                                                 overlay.addClass( 'section-open' );
697                                                 position = content.offset().top;
698                                                 scroll = container.scrollTop();
699                                                 content.css( 'margin-top', ( headerActionsHeight - position - scroll ) );
700                                                 resizeContentHeight();
701                                                 sectionTitle.attr( 'tabindex', '-1' );
702                                                 backBtn.attr( 'tabindex', '0' );
703                                                 backBtn.focus();
704                                                 if ( args.completeCallback ) {
705                                                         args.completeCallback();
706                                                 }
707
708                                                 // Fix the height after browser resize.
709                                                 $( window ).on( 'resize.customizer-section', _.debounce( resizeContentHeight, 100 ) );
710
711                                                 setTimeout( _.bind( section._recalculateTopMargin, section ), 0 );
712                                         };
713                                 }
714
715                                 if ( ! args.allowMultiple ) {
716                                         api.section.each( function ( otherSection ) {
717                                                 if ( otherSection !== section ) {
718                                                         otherSection.collapse( { duration: args.duration } );
719                                                 }
720                                         });
721                                 }
722
723                                 if ( section.panel() ) {
724                                         api.panel( section.panel() ).expand({
725                                                 duration: args.duration,
726                                                 completeCallback: expand
727                                         });
728                                 } else {
729                                         api.panel.each( function( panel ) {
730                                                 panel.collapse();
731                                         });
732                                         expand();
733                                 }
734
735                         } else if ( ! expanded && section.container.hasClass( 'open' ) ) {
736                                 section.container.removeClass( 'open' );
737                                 overlay.removeClass( 'section-open' );
738                                 content.css( 'margin-top', '' );
739                                 container.scrollTop( 0 );
740                                 backBtn.attr( 'tabindex', '-1' );
741                                 sectionTitle.attr( 'tabindex', '0' );
742                                 sectionTitle.focus();
743                                 if ( args.completeCallback ) {
744                                         args.completeCallback();
745                                 }
746                                 $( window ).off( 'resize.customizer-section' );
747                         } else {
748                                 if ( args.completeCallback ) {
749                                         args.completeCallback();
750                                 }
751                         }
752                 },
753
754                 /**
755                  * Recalculate the top margin.
756                  *
757                  * @since 4.4.0
758                  * @private
759                  */
760                 _recalculateTopMargin: function() {
761                         var section = this, content, offset, headerActionsHeight;
762                         content = section.container.find( '.accordion-section-content' );
763                         if ( 0 === content.length ) {
764                                 return;
765                         }
766                         headerActionsHeight = $( '#customize-header-actions' ).height();
767                         offset = ( content.offset().top - headerActionsHeight );
768                         if ( 0 < offset ) {
769                                 content.css( 'margin-top', ( parseInt( content.css( 'margin-top' ), 10 ) - offset ) );
770                         }
771                 }
772         });
773
774         /**
775          * wp.customize.ThemesSection
776          *
777          * Custom section for themes that functions similarly to a backwards panel,
778          * and also handles the theme-details view rendering and navigation.
779          *
780          * @constructor
781          * @augments wp.customize.Section
782          * @augments wp.customize.Container
783          */
784         api.ThemesSection = api.Section.extend({
785                 currentTheme: '',
786                 overlay: '',
787                 template: '',
788                 screenshotQueue: null,
789                 $window: $( window ),
790
791                 /**
792                  * @since 4.2.0
793                  */
794                 initialize: function () {
795                         this.$customizeSidebar = $( '.wp-full-overlay-sidebar-content:first' );
796                         return api.Section.prototype.initialize.apply( this, arguments );
797                 },
798
799                 /**
800                  * @since 4.2.0
801                  */
802                 ready: function () {
803                         var section = this;
804                         section.overlay = section.container.find( '.theme-overlay' );
805                         section.template = wp.template( 'customize-themes-details-view' );
806
807                         // Bind global keyboard events.
808                         $( 'body' ).on( 'keyup', function( event ) {
809                                 if ( ! section.overlay.find( '.theme-wrap' ).is( ':visible' ) ) {
810                                         return;
811                                 }
812
813                                 // Pressing the right arrow key fires a theme:next event
814                                 if ( 39 === event.keyCode ) {
815                                         section.nextTheme();
816                                 }
817
818                                 // Pressing the left arrow key fires a theme:previous event
819                                 if ( 37 === event.keyCode ) {
820                                         section.previousTheme();
821                                 }
822
823                                 // Pressing the escape key fires a theme:collapse event
824                                 if ( 27 === event.keyCode ) {
825                                         section.closeDetails();
826                                 }
827                         });
828
829                         _.bindAll( this, 'renderScreenshots' );
830                 },
831
832                 /**
833                  * Override Section.isContextuallyActive method.
834                  *
835                  * Ignore the active states' of the contained theme controls, and just
836                  * use the section's own active state instead. This ensures empty search
837                  * results for themes to cause the section to become inactive.
838                  *
839                  * @since 4.2.0
840                  *
841                  * @returns {Boolean}
842                  */
843                 isContextuallyActive: function () {
844                         return this.active();
845                 },
846
847                 /**
848                  * @since 4.2.0
849                  */
850                 attachEvents: function () {
851                         var section = this;
852
853                         // Expand/Collapse section/panel.
854                         section.container.find( '.change-theme, .customize-theme' ).on( 'click keydown', function( event ) {
855                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
856                                         return;
857                                 }
858                                 event.preventDefault(); // Keep this AFTER the key filter above
859
860                                 if ( section.expanded() ) {
861                                         section.collapse();
862                                 } else {
863                                         section.expand();
864                                 }
865                         });
866
867                         // Theme navigation in details view.
868                         section.container.on( 'click keydown', '.left', function( event ) {
869                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
870                                         return;
871                                 }
872
873                                 event.preventDefault(); // Keep this AFTER the key filter above
874
875                                 section.previousTheme();
876                         });
877
878                         section.container.on( 'click keydown', '.right', function( event ) {
879                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
880                                         return;
881                                 }
882
883                                 event.preventDefault(); // Keep this AFTER the key filter above
884
885                                 section.nextTheme();
886                         });
887
888                         section.container.on( 'click keydown', '.theme-backdrop, .close', function( event ) {
889                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
890                                         return;
891                                 }
892
893                                 event.preventDefault(); // Keep this AFTER the key filter above
894
895                                 section.closeDetails();
896                         });
897
898                         var renderScreenshots = _.throttle( _.bind( section.renderScreenshots, this ), 100 );
899                         section.container.on( 'input', '#themes-filter', function( event ) {
900                                 var count,
901                                         term = event.currentTarget.value.toLowerCase().trim().replace( '-', ' ' ),
902                                         controls = section.controls();
903
904                                 _.each( controls, function( control ) {
905                                         control.filter( term );
906                                 });
907
908                                 renderScreenshots();
909
910                                 // Update theme count.
911                                 count = section.container.find( 'li.customize-control:visible' ).length;
912                                 section.container.find( '.theme-count' ).text( count );
913                         });
914
915                         // Pre-load the first 3 theme screenshots.
916                         api.bind( 'ready', function () {
917                                 _.each( section.controls().slice( 0, 3 ), function ( control ) {
918                                         var img, src = control.params.theme.screenshot[0];
919                                         if ( src ) {
920                                                 img = new Image();
921                                                 img.src = src;
922                                         }
923                                 });
924                         });
925                 },
926
927                 /**
928                  * Update UI to reflect expanded state
929                  *
930                  * @since 4.2.0
931                  *
932                  * @param {Boolean}  expanded
933                  * @param {Object}   args
934                  * @param {Boolean}  args.unchanged
935                  * @param {Callback} args.completeCallback
936                  */
937                 onChangeExpanded: function ( expanded, args ) {
938
939                         // Immediately call the complete callback if there were no changes
940                         if ( args.unchanged ) {
941                                 if ( args.completeCallback ) {
942                                         args.completeCallback();
943                                 }
944                                 return;
945                         }
946
947                         // Note: there is a second argument 'args' passed
948                         var position, scroll,
949                                 panel = this,
950                                 section = panel.container.closest( '.accordion-section' ),
951                                 overlay = section.closest( '.wp-full-overlay' ),
952                                 container = section.closest( '.wp-full-overlay-sidebar-content' ),
953                                 siblings = container.find( '.open' ),
954                                 customizeBtn = section.find( '.customize-theme' ),
955                                 changeBtn = section.find( '.change-theme' ),
956                                 content = section.find( '.control-panel-content' );
957
958                         if ( expanded ) {
959
960                                 // Collapse any sibling sections/panels
961                                 api.section.each( function ( otherSection ) {
962                                         if ( otherSection !== panel ) {
963                                                 otherSection.collapse( { duration: args.duration } );
964                                         }
965                                 });
966                                 api.panel.each( function ( otherPanel ) {
967                                         otherPanel.collapse( { duration: 0 } );
968                                 });
969
970                                 content.show( 0, function() {
971                                         position = content.offset().top;
972                                         scroll = container.scrollTop();
973                                         content.css( 'margin-top', ( $( '#customize-header-actions' ).height() - position - scroll ) );
974                                         section.addClass( 'current-panel' );
975                                         overlay.addClass( 'in-themes-panel' );
976                                         container.scrollTop( 0 );
977                                         _.delay( panel.renderScreenshots, 10 ); // Wait for the controls
978                                         panel.$customizeSidebar.on( 'scroll.customize-themes-section', _.throttle( panel.renderScreenshots, 300 ) );
979                                         if ( args.completeCallback ) {
980                                                 args.completeCallback();
981                                         }
982                                 } );
983                                 customizeBtn.focus();
984                         } else {
985                                 siblings.removeClass( 'open' );
986                                 section.removeClass( 'current-panel' );
987                                 overlay.removeClass( 'in-themes-panel' );
988                                 panel.$customizeSidebar.off( 'scroll.customize-themes-section' );
989                                 content.delay( 180 ).hide( 0, function() {
990                                         content.css( 'margin-top', 'inherit' ); // Reset
991                                         if ( args.completeCallback ) {
992                                                 args.completeCallback();
993                                         }
994                                 } );
995                                 customizeBtn.attr( 'tabindex', '0' );
996                                 changeBtn.focus();
997                                 container.scrollTop( 0 );
998                         }
999                 },
1000
1001                 /**
1002                  * Recalculate the top margin.
1003                  *
1004                  * @since 4.4.0
1005                  * @private
1006                  */
1007                 _recalculateTopMargin: function() {
1008                         api.Panel.prototype._recalculateTopMargin.call( this );
1009                 },
1010
1011                 /**
1012                  * Render control's screenshot if the control comes into view.
1013                  *
1014                  * @since 4.2.0
1015                  */
1016                 renderScreenshots: function( ) {
1017                         var section = this;
1018
1019                         // Fill queue initially.
1020                         if ( section.screenshotQueue === null ) {
1021                                 section.screenshotQueue = section.controls();
1022                         }
1023
1024                         // Are all screenshots rendered?
1025                         if ( ! section.screenshotQueue.length ) {
1026                                 return;
1027                         }
1028
1029                         section.screenshotQueue = _.filter( section.screenshotQueue, function( control ) {
1030                                 var $imageWrapper = control.container.find( '.theme-screenshot' ),
1031                                         $image = $imageWrapper.find( 'img' );
1032
1033                                 if ( ! $image.length ) {
1034                                         return false;
1035                                 }
1036
1037                                 if ( $image.is( ':hidden' ) ) {
1038                                         return true;
1039                                 }
1040
1041                                 // Based on unveil.js.
1042                                 var wt = section.$window.scrollTop(),
1043                                         wb = wt + section.$window.height(),
1044                                         et = $image.offset().top,
1045                                         ih = $imageWrapper.height(),
1046                                         eb = et + ih,
1047                                         threshold = ih * 3,
1048                                         inView = eb >= wt - threshold && et <= wb + threshold;
1049
1050                                 if ( inView ) {
1051                                         control.container.trigger( 'render-screenshot' );
1052                                 }
1053
1054                                 // If the image is in view return false so it's cleared from the queue.
1055                                 return ! inView;
1056                         } );
1057                 },
1058
1059                 /**
1060                  * Advance the modal to the next theme.
1061                  *
1062                  * @since 4.2.0
1063                  */
1064                 nextTheme: function () {
1065                         var section = this;
1066                         if ( section.getNextTheme() ) {
1067                                 section.showDetails( section.getNextTheme(), function() {
1068                                         section.overlay.find( '.right' ).focus();
1069                                 } );
1070                         }
1071                 },
1072
1073                 /**
1074                  * Get the next theme model.
1075                  *
1076                  * @since 4.2.0
1077                  */
1078                 getNextTheme: function () {
1079                         var control, next;
1080                         control = api.control( 'theme_' + this.currentTheme );
1081                         next = control.container.next( 'li.customize-control-theme' );
1082                         if ( ! next.length ) {
1083                                 return false;
1084                         }
1085                         next = next[0].id.replace( 'customize-control-', '' );
1086                         control = api.control( next );
1087
1088                         return control.params.theme;
1089                 },
1090
1091                 /**
1092                  * Advance the modal to the previous theme.
1093                  *
1094                  * @since 4.2.0
1095                  */
1096                 previousTheme: function () {
1097                         var section = this;
1098                         if ( section.getPreviousTheme() ) {
1099                                 section.showDetails( section.getPreviousTheme(), function() {
1100                                         section.overlay.find( '.left' ).focus();
1101                                 } );
1102                         }
1103                 },
1104
1105                 /**
1106                  * Get the previous theme model.
1107                  *
1108                  * @since 4.2.0
1109                  */
1110                 getPreviousTheme: function () {
1111                         var control, previous;
1112                         control = api.control( 'theme_' + this.currentTheme );
1113                         previous = control.container.prev( 'li.customize-control-theme' );
1114                         if ( ! previous.length ) {
1115                                 return false;
1116                         }
1117                         previous = previous[0].id.replace( 'customize-control-', '' );
1118                         control = api.control( previous );
1119
1120                         return control.params.theme;
1121                 },
1122
1123                 /**
1124                  * Disable buttons when we're viewing the first or last theme.
1125                  *
1126                  * @since 4.2.0
1127                  */
1128                 updateLimits: function () {
1129                         if ( ! this.getNextTheme() ) {
1130                                 this.overlay.find( '.right' ).addClass( 'disabled' );
1131                         }
1132                         if ( ! this.getPreviousTheme() ) {
1133                                 this.overlay.find( '.left' ).addClass( 'disabled' );
1134                         }
1135                 },
1136
1137                 /**
1138                  * Render & show the theme details for a given theme model.
1139                  *
1140                  * @since 4.2.0
1141                  *
1142                  * @param {Object}   theme
1143                  */
1144                 showDetails: function ( theme, callback ) {
1145                         var section = this;
1146                         callback = callback || function(){};
1147                         section.currentTheme = theme.id;
1148                         section.overlay.html( section.template( theme ) )
1149                                 .fadeIn( 'fast' )
1150                                 .focus();
1151                         $( 'body' ).addClass( 'modal-open' );
1152                         section.containFocus( section.overlay );
1153                         section.updateLimits();
1154                         callback();
1155                 },
1156
1157                 /**
1158                  * Close the theme details modal.
1159                  *
1160                  * @since 4.2.0
1161                  */
1162                 closeDetails: function () {
1163                         $( 'body' ).removeClass( 'modal-open' );
1164                         this.overlay.fadeOut( 'fast' );
1165                         api.control( 'theme_' + this.currentTheme ).focus();
1166                 },
1167
1168                 /**
1169                  * Keep tab focus within the theme details modal.
1170                  *
1171                  * @since 4.2.0
1172                  */
1173                 containFocus: function( el ) {
1174                         var tabbables;
1175
1176                         el.on( 'keydown', function( event ) {
1177
1178                                 // Return if it's not the tab key
1179                                 // When navigating with prev/next focus is already handled
1180                                 if ( 9 !== event.keyCode ) {
1181                                         return;
1182                                 }
1183
1184                                 // uses jQuery UI to get the tabbable elements
1185                                 tabbables = $( ':tabbable', el );
1186
1187                                 // Keep focus within the overlay
1188                                 if ( tabbables.last()[0] === event.target && ! event.shiftKey ) {
1189                                         tabbables.first().focus();
1190                                         return false;
1191                                 } else if ( tabbables.first()[0] === event.target && event.shiftKey ) {
1192                                         tabbables.last().focus();
1193                                         return false;
1194                                 }
1195                         });
1196                 }
1197         });
1198
1199         /**
1200          * @since 4.1.0
1201          *
1202          * @class
1203          * @augments wp.customize.Class
1204          */
1205         api.Panel = Container.extend({
1206                 containerType: 'panel',
1207
1208                 /**
1209                  * @since 4.1.0
1210                  *
1211                  * @param {string}         id - The ID for the panel.
1212                  * @param {object}         options - Object containing one property: params.
1213                  * @param {object}         options.params - Object containing the following properties.
1214                  * @param {string}         options.params.title - Title shown when panel is collapsed and expanded.
1215                  * @param {string=}        [options.params.description] - Description shown at the top of the panel.
1216                  * @param {number=100}     [options.params.priority] - The sort priority for the panel.
1217                  * @param {string=default} [options.params.type] - The type of the panel. See wp.customize.panelConstructor.
1218                  * @param {string=}        [options.params.content] - The markup to be used for the panel container. If empty, a JS template is used.
1219                  * @param {boolean=true}   [options.params.active] - Whether the panel is active or not.
1220                  */
1221                 initialize: function ( id, options ) {
1222                         var panel = this;
1223                         Container.prototype.initialize.call( panel, id, options );
1224                         panel.embed();
1225                         panel.deferred.embedded.done( function () {
1226                                 panel.ready();
1227                         });
1228                 },
1229
1230                 /**
1231                  * Embed the container in the DOM when any parent panel is ready.
1232                  *
1233                  * @since 4.1.0
1234                  */
1235                 embed: function () {
1236                         var panel = this,
1237                                 parentContainer = $( '#customize-theme-controls > ul' ); // @todo This should be defined elsewhere, and to be configurable
1238
1239                         if ( ! panel.container.parent().is( parentContainer ) ) {
1240                                 parentContainer.append( panel.container );
1241                                 panel.renderContent();
1242                         }
1243
1244                         api.bind( 'pane-contents-reflowed', _.debounce( function() {
1245                                 panel._recalculateTopMargin();
1246                         }, 100 ) );
1247
1248                         panel.deferred.embedded.resolve();
1249                 },
1250
1251                 /**
1252                  * @since 4.1.0
1253                  */
1254                 attachEvents: function () {
1255                         var meta, panel = this;
1256
1257                         // Expand/Collapse accordion sections on click.
1258                         panel.container.find( '.accordion-section-title' ).on( 'click keydown', function( event ) {
1259                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1260                                         return;
1261                                 }
1262                                 event.preventDefault(); // Keep this AFTER the key filter above
1263
1264                                 if ( ! panel.expanded() ) {
1265                                         panel.expand();
1266                                 }
1267                         });
1268
1269                         // Close panel.
1270                         panel.container.find( '.customize-panel-back' ).on( 'click keydown', function( event ) {
1271                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1272                                         return;
1273                                 }
1274                                 event.preventDefault(); // Keep this AFTER the key filter above
1275
1276                                 if ( panel.expanded() ) {
1277                                         panel.collapse();
1278                                 }
1279                         });
1280
1281                         meta = panel.container.find( '.panel-meta:first' );
1282
1283                         meta.find( '> .accordion-section-title .customize-help-toggle' ).on( 'click keydown', function( event ) {
1284                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1285                                         return;
1286                                 }
1287                                 event.preventDefault(); // Keep this AFTER the key filter above
1288
1289                                 meta = panel.container.find( '.panel-meta' );
1290                                 if ( meta.hasClass( 'cannot-expand' ) ) {
1291                                         return;
1292                                 }
1293
1294                                 var content = meta.find( '.customize-panel-description:first' );
1295                                 if ( meta.hasClass( 'open' ) ) {
1296                                         meta.toggleClass( 'open' );
1297                                         content.slideUp( panel.defaultExpandedArguments.duration );
1298                                         $( this ).attr( 'aria-expanded', false );
1299                                 } else {
1300                                         content.slideDown( panel.defaultExpandedArguments.duration );
1301                                         meta.toggleClass( 'open' );
1302                                         $( this ).attr( 'aria-expanded', true );
1303                                 }
1304                         });
1305
1306                 },
1307
1308                 /**
1309                  * Get the sections that are associated with this panel, sorted by their priority Value.
1310                  *
1311                  * @since 4.1.0
1312                  *
1313                  * @returns {Array}
1314                  */
1315                 sections: function () {
1316                         return this._children( 'panel', 'section' );
1317                 },
1318
1319                 /**
1320                  * Return whether this panel has any active sections.
1321                  *
1322                  * @since 4.1.0
1323                  *
1324                  * @returns {boolean}
1325                  */
1326                 isContextuallyActive: function () {
1327                         var panel = this,
1328                                 sections = panel.sections(),
1329                                 activeCount = 0;
1330                         _( sections ).each( function ( section ) {
1331                                 if ( section.active() && section.isContextuallyActive() ) {
1332                                         activeCount += 1;
1333                                 }
1334                         } );
1335                         return ( activeCount !== 0 );
1336                 },
1337
1338                 /**
1339                  * Update UI to reflect expanded state
1340                  *
1341                  * @since 4.1.0
1342                  *
1343                  * @param {Boolean}  expanded
1344                  * @param {Object}   args
1345                  * @param {Boolean}  args.unchanged
1346                  * @param {Function} args.completeCallback
1347                  */
1348                 onChangeExpanded: function ( expanded, args ) {
1349
1350                         // Immediately call the complete callback if there were no changes
1351                         if ( args.unchanged ) {
1352                                 if ( args.completeCallback ) {
1353                                         args.completeCallback();
1354                                 }
1355                                 return;
1356                         }
1357
1358                         // Note: there is a second argument 'args' passed
1359                         var position, scroll,
1360                                 panel = this,
1361                                 accordionSection = panel.container.closest( '.accordion-section' ),
1362                                 overlay = accordionSection.closest( '.wp-full-overlay' ),
1363                                 container = accordionSection.closest( '.wp-full-overlay-sidebar-content' ),
1364                                 siblings = container.find( '.open' ),
1365                                 topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ),
1366                                 backBtn = accordionSection.find( '.customize-panel-back' ),
1367                                 panelTitle = accordionSection.find( '.accordion-section-title' ).first(),
1368                                 content = accordionSection.find( '.control-panel-content' ),
1369                                 headerActionsHeight = $( '#customize-header-actions' ).height();
1370
1371                         if ( expanded ) {
1372
1373                                 // Collapse any sibling sections/panels
1374                                 api.section.each( function ( section ) {
1375                                         if ( panel.id !== section.panel() ) {
1376                                                 section.collapse( { duration: 0 } );
1377                                         }
1378                                 });
1379                                 api.panel.each( function ( otherPanel ) {
1380                                         if ( panel !== otherPanel ) {
1381                                                 otherPanel.collapse( { duration: 0 } );
1382                                         }
1383                                 });
1384
1385                                 content.show( 0, function() {
1386                                         content.parent().show();
1387                                         position = content.offset().top;
1388                                         scroll = container.scrollTop();
1389                                         content.css( 'margin-top', ( headerActionsHeight - position - scroll ) );
1390                                         accordionSection.addClass( 'current-panel' );
1391                                         overlay.addClass( 'in-sub-panel' );
1392                                         container.scrollTop( 0 );
1393                                         if ( args.completeCallback ) {
1394                                                 args.completeCallback();
1395                                         }
1396                                 } );
1397                                 topPanel.attr( 'tabindex', '-1' );
1398                                 backBtn.attr( 'tabindex', '0' );
1399                                 backBtn.focus();
1400                                 panel._recalculateTopMargin();
1401                         } else {
1402                                 siblings.removeClass( 'open' );
1403                                 accordionSection.removeClass( 'current-panel' );
1404                                 overlay.removeClass( 'in-sub-panel' );
1405                                 content.delay( 180 ).hide( 0, function() {
1406                                         content.css( 'margin-top', 'inherit' ); // Reset
1407                                         if ( args.completeCallback ) {
1408                                                 args.completeCallback();
1409                                         }
1410                                 } );
1411                                 topPanel.attr( 'tabindex', '0' );
1412                                 backBtn.attr( 'tabindex', '-1' );
1413                                 panelTitle.focus();
1414                                 container.scrollTop( 0 );
1415                         }
1416                 },
1417
1418                 /**
1419                  * Recalculate the top margin.
1420                  *
1421                  * @since 4.4.0
1422                  * @private
1423                  */
1424                 _recalculateTopMargin: function() {
1425                         var panel = this, headerActionsHeight, content, accordionSection;
1426                         headerActionsHeight = $( '#customize-header-actions' ).height();
1427                         accordionSection = panel.container.closest( '.accordion-section' );
1428                         content = accordionSection.find( '.control-panel-content' );
1429                         content.css( 'margin-top', ( parseInt( content.css( 'margin-top' ), 10 ) - ( content.offset().top - headerActionsHeight ) ) );
1430                 },
1431
1432                 /**
1433                  * Render the panel from its JS template, if it exists.
1434                  *
1435                  * The panel's container must already exist in the DOM.
1436                  *
1437                  * @since 4.3.0
1438                  */
1439                 renderContent: function () {
1440                         var template,
1441                                 panel = this;
1442
1443                         // Add the content to the container.
1444                         if ( 0 !== $( '#tmpl-' + panel.templateSelector + '-content' ).length ) {
1445                                 template = wp.template( panel.templateSelector + '-content' );
1446                         } else {
1447                                 template = wp.template( 'customize-panel-default-content' );
1448                         }
1449                         if ( template && panel.container ) {
1450                                 panel.container.find( '.accordion-sub-container' ).html( template( panel.params ) );
1451                         }
1452                 }
1453         });
1454
1455         /**
1456          * A Customizer Control.
1457          *
1458          * A control provides a UI element that allows a user to modify a Customizer Setting.
1459          *
1460          * @see PHP class WP_Customize_Control.
1461          *
1462          * @class
1463          * @augments wp.customize.Class
1464          *
1465          * @param {string} id                              Unique identifier for the control instance.
1466          * @param {object} options                         Options hash for the control instance.
1467          * @param {object} options.params
1468          * @param {object} options.params.type             Type of control (e.g. text, radio, dropdown-pages, etc.)
1469          * @param {string} options.params.content          The HTML content for the control.
1470          * @param {string} options.params.priority         Order of priority to show the control within the section.
1471          * @param {string} options.params.active
1472          * @param {string} options.params.section          The ID of the section the control belongs to.
1473          * @param {string} options.params.settings.default The ID of the setting the control relates to.
1474          * @param {string} options.params.settings.data
1475          * @param {string} options.params.label
1476          * @param {string} options.params.description
1477          * @param {string} options.params.instanceNumber Order in which this instance was created in relation to other instances.
1478          */
1479         api.Control = api.Class.extend({
1480                 defaultActiveArguments: { duration: 'fast', completeCallback: $.noop },
1481
1482                 initialize: function( id, options ) {
1483                         var control = this,
1484                                 nodes, radios, settings;
1485
1486                         control.params = {};
1487                         $.extend( control, options || {} );
1488                         control.id = id;
1489                         control.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' );
1490                         control.templateSelector = 'customize-control-' + control.params.type + '-content';
1491                         control.container = control.params.content ? $( control.params.content ) : $( control.selector );
1492
1493                         control.deferred = {
1494                                 embedded: new $.Deferred()
1495                         };
1496                         control.section = new api.Value();
1497                         control.priority = new api.Value();
1498                         control.active = new api.Value();
1499                         control.activeArgumentsQueue = [];
1500                         control.notifications = new api.Values({ defaultConstructor: api.Notification });
1501
1502                         control.elements = [];
1503
1504                         nodes  = control.container.find('[data-customize-setting-link]');
1505                         radios = {};
1506
1507                         nodes.each( function() {
1508                                 var node = $( this ),
1509                                         name;
1510
1511                                 if ( node.is( ':radio' ) ) {
1512                                         name = node.prop( 'name' );
1513                                         if ( radios[ name ] ) {
1514                                                 return;
1515                                         }
1516
1517                                         radios[ name ] = true;
1518                                         node = nodes.filter( '[name="' + name + '"]' );
1519                                 }
1520
1521                                 api( node.data( 'customizeSettingLink' ), function( setting ) {
1522                                         var element = new api.Element( node );
1523                                         control.elements.push( element );
1524                                         element.sync( setting );
1525                                         element.set( setting() );
1526                                 });
1527                         });
1528
1529                         control.active.bind( function ( active ) {
1530                                 var args = control.activeArgumentsQueue.shift();
1531                                 args = $.extend( {}, control.defaultActiveArguments, args );
1532                                 control.onChangeActive( active, args );
1533                         } );
1534
1535                         control.section.set( control.params.section );
1536                         control.priority.set( isNaN( control.params.priority ) ? 10 : control.params.priority );
1537                         control.active.set( control.params.active );
1538
1539                         api.utils.bubbleChildValueChanges( control, [ 'section', 'priority', 'active' ] );
1540
1541                         /*
1542                          * After all settings related to the control are available,
1543                          * make them available on the control and embed the control into the page.
1544                          */
1545                         settings = $.map( control.params.settings, function( value ) {
1546                                 return value;
1547                         });
1548
1549                         if ( 0 === settings.length ) {
1550                                 control.setting = null;
1551                                 control.settings = {};
1552                                 control.embed();
1553                         } else {
1554                                 api.apply( api, settings.concat( function() {
1555                                         var key;
1556
1557                                         control.settings = {};
1558                                         for ( key in control.params.settings ) {
1559                                                 control.settings[ key ] = api( control.params.settings[ key ] );
1560                                         }
1561
1562                                         control.setting = control.settings['default'] || null;
1563
1564                                         // Add setting notifications to the control notification.
1565                                         _.each( control.settings, function( setting ) {
1566                                                 setting.notifications.bind( 'add', function( settingNotification ) {
1567                                                         var controlNotification, code, params;
1568                                                         code = setting.id + ':' + settingNotification.code;
1569                                                         params = _.extend(
1570                                                                 {},
1571                                                                 settingNotification,
1572                                                                 {
1573                                                                         setting: setting.id
1574                                                                 }
1575                                                         );
1576                                                         controlNotification = new api.Notification( code, params );
1577                                                         control.notifications.add( controlNotification.code, controlNotification );
1578                                                 } );
1579                                                 setting.notifications.bind( 'remove', function( settingNotification ) {
1580                                                         control.notifications.remove( setting.id + ':' + settingNotification.code );
1581                                                 } );
1582                                         } );
1583
1584                                         control.embed();
1585                                 }) );
1586                         }
1587
1588                         // After the control is embedded on the page, invoke the "ready" method.
1589                         control.deferred.embedded.done( function () {
1590                                 /*
1591                                  * Note that this debounced/deferred rendering is needed for two reasons:
1592                                  * 1) The 'remove' event is triggered just _before_ the notification is actually removed.
1593                                  * 2) Improve performance when adding/removing multiple notifications at a time.
1594                                  */
1595                                 var debouncedRenderNotifications = _.debounce( function renderNotifications() {
1596                                         control.renderNotifications();
1597                                 } );
1598                                 control.notifications.bind( 'add', function( notification ) {
1599                                         wp.a11y.speak( notification.message, 'assertive' );
1600                                         debouncedRenderNotifications();
1601                                 } );
1602                                 control.notifications.bind( 'remove', debouncedRenderNotifications );
1603                                 control.renderNotifications();
1604
1605                                 control.ready();
1606                         });
1607                 },
1608
1609                 /**
1610                  * Embed the control into the page.
1611                  */
1612                 embed: function () {
1613                         var control = this,
1614                                 inject;
1615
1616                         // Watch for changes to the section state
1617                         inject = function ( sectionId ) {
1618                                 var parentContainer;
1619                                 if ( ! sectionId ) { // @todo allow a control to be embedded without a section, for instance a control embedded in the front end.
1620                                         return;
1621                                 }
1622                                 // Wait for the section to be registered
1623                                 api.section( sectionId, function ( section ) {
1624                                         // Wait for the section to be ready/initialized
1625                                         section.deferred.embedded.done( function () {
1626                                                 parentContainer = section.container.find( 'ul:first' );
1627                                                 if ( ! control.container.parent().is( parentContainer ) ) {
1628                                                         parentContainer.append( control.container );
1629                                                         control.renderContent();
1630                                                 }
1631                                                 control.deferred.embedded.resolve();
1632                                         });
1633                                 });
1634                         };
1635                         control.section.bind( inject );
1636                         inject( control.section.get() );
1637                 },
1638
1639                 /**
1640                  * Triggered when the control's markup has been injected into the DOM.
1641                  *
1642                  * @abstract
1643                  */
1644                 ready: function() {},
1645
1646                 /**
1647                  * Get the element inside of a control's container that contains the validation error message.
1648                  *
1649                  * Control subclasses may override this to return the proper container to render notifications into.
1650                  * Injects the notification container for existing controls that lack the necessary container,
1651                  * including special handling for nav menu items and widgets.
1652                  *
1653                  * @since 4.6.0
1654                  * @returns {jQuery} Setting validation message element.
1655                  * @this {wp.customize.Control}
1656                  */
1657                 getNotificationsContainerElement: function() {
1658                         var control = this, controlTitle, notificationsContainer;
1659
1660                         notificationsContainer = control.container.find( '.customize-control-notifications-container:first' );
1661                         if ( notificationsContainer.length ) {
1662                                 return notificationsContainer;
1663                         }
1664
1665                         notificationsContainer = $( '<div class="customize-control-notifications-container"></div>' );
1666
1667                         if ( control.container.hasClass( 'customize-control-nav_menu_item' ) ) {
1668                                 control.container.find( '.menu-item-settings:first' ).prepend( notificationsContainer );
1669                         } else if ( control.container.hasClass( 'customize-control-widget_form' ) ) {
1670                                 control.container.find( '.widget-inside:first' ).prepend( notificationsContainer );
1671                         } else {
1672                                 controlTitle = control.container.find( '.customize-control-title' );
1673                                 if ( controlTitle.length ) {
1674                                         controlTitle.after( notificationsContainer );
1675                                 } else {
1676                                         control.container.prepend( notificationsContainer );
1677                                 }
1678                         }
1679                         return notificationsContainer;
1680                 },
1681
1682                 /**
1683                  * Render notifications.
1684                  *
1685                  * Renders the `control.notifications` into the control's container.
1686                  * Control subclasses may override this method to do their own handling
1687                  * of rendering notifications.
1688                  *
1689                  * @since 4.6.0
1690                  * @this {wp.customize.Control}
1691                  */
1692                 renderNotifications: function() {
1693                         var control = this, container, notifications, hasError = false;
1694                         container = control.getNotificationsContainerElement();
1695                         if ( ! container || ! container.length ) {
1696                                 return;
1697                         }
1698                         notifications = [];
1699                         control.notifications.each( function( notification ) {
1700                                 notifications.push( notification );
1701                                 if ( 'error' === notification.type ) {
1702                                         hasError = true;
1703                                 }
1704                         } );
1705
1706                         if ( 0 === notifications.length ) {
1707                                 container.stop().slideUp( 'fast' );
1708                         } else {
1709                                 container.stop().slideDown( 'fast', null, function() {
1710                                         $( this ).css( 'height', 'auto' );
1711                                 } );
1712                         }
1713
1714                         if ( ! control.notificationsTemplate ) {
1715                                 control.notificationsTemplate = wp.template( 'customize-control-notifications' );
1716                         }
1717
1718                         control.container.toggleClass( 'has-notifications', 0 !== notifications.length );
1719                         control.container.toggleClass( 'has-error', hasError );
1720                         container.empty().append( $.trim(
1721                                 control.notificationsTemplate( { notifications: notifications, altNotice: Boolean( control.altNotice ) } )
1722                         ) );
1723                 },
1724
1725                 /**
1726                  * Normal controls do not expand, so just expand its parent
1727                  *
1728                  * @param {Object} [params]
1729                  */
1730                 expand: function ( params ) {
1731                         api.section( this.section() ).expand( params );
1732                 },
1733
1734                 /**
1735                  * Bring the containing section and panel into view and then
1736                  * this control into view, focusing on the first input.
1737                  */
1738                 focus: focus,
1739
1740                 /**
1741                  * Update UI in response to a change in the control's active state.
1742                  * This does not change the active state, it merely handles the behavior
1743                  * for when it does change.
1744                  *
1745                  * @since 4.1.0
1746                  *
1747                  * @param {Boolean}  active
1748                  * @param {Object}   args
1749                  * @param {Number}   args.duration
1750                  * @param {Callback} args.completeCallback
1751                  */
1752                 onChangeActive: function ( active, args ) {
1753                         if ( args.unchanged ) {
1754                                 if ( args.completeCallback ) {
1755                                         args.completeCallback();
1756                                 }
1757                                 return;
1758                         }
1759
1760                         if ( ! $.contains( document, this.container[0] ) ) {
1761                                 // jQuery.fn.slideUp is not hiding an element if it is not in the DOM
1762                                 this.container.toggle( active );
1763                                 if ( args.completeCallback ) {
1764                                         args.completeCallback();
1765                                 }
1766                         } else if ( active ) {
1767                                 this.container.slideDown( args.duration, args.completeCallback );
1768                         } else {
1769                                 this.container.slideUp( args.duration, args.completeCallback );
1770                         }
1771                 },
1772
1773                 /**
1774                  * @deprecated 4.1.0 Use this.onChangeActive() instead.
1775                  */
1776                 toggle: function ( active ) {
1777                         return this.onChangeActive( active, this.defaultActiveArguments );
1778                 },
1779
1780                 /**
1781                  * Shorthand way to enable the active state.
1782                  *
1783                  * @since 4.1.0
1784                  *
1785                  * @param {Object} [params]
1786                  * @returns {Boolean} false if already active
1787                  */
1788                 activate: Container.prototype.activate,
1789
1790                 /**
1791                  * Shorthand way to disable the active state.
1792                  *
1793                  * @since 4.1.0
1794                  *
1795                  * @param {Object} [params]
1796                  * @returns {Boolean} false if already inactive
1797                  */
1798                 deactivate: Container.prototype.deactivate,
1799
1800                 /**
1801                  * Re-use _toggleActive from Container class.
1802                  *
1803                  * @access private
1804                  */
1805                 _toggleActive: Container.prototype._toggleActive,
1806
1807                 dropdownInit: function() {
1808                         var control      = this,
1809                                 statuses     = this.container.find('.dropdown-status'),
1810                                 params       = this.params,
1811                                 toggleFreeze = false,
1812                                 update       = function( to ) {
1813                                         if ( typeof to === 'string' && params.statuses && params.statuses[ to ] )
1814                                                 statuses.html( params.statuses[ to ] ).show();
1815                                         else
1816                                                 statuses.hide();
1817                                 };
1818
1819                         // Support the .dropdown class to open/close complex elements
1820                         this.container.on( 'click keydown', '.dropdown', function( event ) {
1821                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1822                                         return;
1823                                 }
1824
1825                                 event.preventDefault();
1826
1827                                 if (!toggleFreeze)
1828                                         control.container.toggleClass('open');
1829
1830                                 if ( control.container.hasClass('open') )
1831                                         control.container.parent().parent().find('li.library-selected').focus();
1832
1833                                 // Don't want to fire focus and click at same time
1834                                 toggleFreeze = true;
1835                                 setTimeout(function () {
1836                                         toggleFreeze = false;
1837                                 }, 400);
1838                         });
1839
1840                         this.setting.bind( update );
1841                         update( this.setting() );
1842                 },
1843
1844                 /**
1845                  * Render the control from its JS template, if it exists.
1846                  *
1847                  * The control's container must already exist in the DOM.
1848                  *
1849                  * @since 4.1.0
1850                  */
1851                 renderContent: function () {
1852                         var template,
1853                                 control = this;
1854
1855                         // Replace the container element's content with the control.
1856                         if ( 0 !== $( '#tmpl-' + control.templateSelector ).length ) {
1857                                 template = wp.template( control.templateSelector );
1858                                 if ( template && control.container ) {
1859                                         control.container.html( template( control.params ) );
1860                                 }
1861                         }
1862                 }
1863         });
1864
1865         /**
1866          * A colorpicker control.
1867          *
1868          * @class
1869          * @augments wp.customize.Control
1870          * @augments wp.customize.Class
1871          */
1872         api.ColorControl = api.Control.extend({
1873                 ready: function() {
1874                         var control = this,
1875                                 picker = this.container.find('.color-picker-hex');
1876
1877                         picker.val( control.setting() ).wpColorPicker({
1878                                 change: function() {
1879                                         control.setting.set( picker.wpColorPicker('color') );
1880                                 },
1881                                 clear: function() {
1882                                         control.setting.set( '' );
1883                                 }
1884                         });
1885
1886                         this.setting.bind( function ( value ) {
1887                                 picker.val( value );
1888                                 picker.wpColorPicker( 'color', value );
1889                         });
1890                 }
1891         });
1892
1893         /**
1894          * A control that implements the media modal.
1895          *
1896          * @class
1897          * @augments wp.customize.Control
1898          * @augments wp.customize.Class
1899          */
1900         api.MediaControl = api.Control.extend({
1901
1902                 /**
1903                  * When the control's DOM structure is ready,
1904                  * set up internal event bindings.
1905                  */
1906                 ready: function() {
1907                         var control = this;
1908                         // Shortcut so that we don't have to use _.bind every time we add a callback.
1909                         _.bindAll( control, 'restoreDefault', 'removeFile', 'openFrame', 'select', 'pausePlayer' );
1910
1911                         // Bind events, with delegation to facilitate re-rendering.
1912                         control.container.on( 'click keydown', '.upload-button', control.openFrame );
1913                         control.container.on( 'click keydown', '.upload-button', control.pausePlayer );
1914                         control.container.on( 'click keydown', '.thumbnail-image img', control.openFrame );
1915                         control.container.on( 'click keydown', '.default-button', control.restoreDefault );
1916                         control.container.on( 'click keydown', '.remove-button', control.pausePlayer );
1917                         control.container.on( 'click keydown', '.remove-button', control.removeFile );
1918                         control.container.on( 'click keydown', '.remove-button', control.cleanupPlayer );
1919
1920                         // Resize the player controls when it becomes visible (ie when section is expanded)
1921                         api.section( control.section() ).container
1922                                 .on( 'expanded', function() {
1923                                         if ( control.player ) {
1924                                                 control.player.setControlsSize();
1925                                         }
1926                                 })
1927                                 .on( 'collapsed', function() {
1928                                         control.pausePlayer();
1929                                 });
1930
1931                         /**
1932                          * Set attachment data and render content.
1933                          *
1934                          * Note that BackgroundImage.prototype.ready applies this ready method
1935                          * to itself. Since BackgroundImage is an UploadControl, the value
1936                          * is the attachment URL instead of the attachment ID. In this case
1937                          * we skip fetching the attachment data because we have no ID available,
1938                          * and it is the responsibility of the UploadControl to set the control's
1939                          * attachmentData before calling the renderContent method.
1940                          *
1941                          * @param {number|string} value Attachment
1942                          */
1943                         function setAttachmentDataAndRenderContent( value ) {
1944                                 var hasAttachmentData = $.Deferred();
1945
1946                                 if ( control.extended( api.UploadControl ) ) {
1947                                         hasAttachmentData.resolve();
1948                                 } else {
1949                                         value = parseInt( value, 10 );
1950                                         if ( _.isNaN( value ) || value <= 0 ) {
1951                                                 delete control.params.attachment;
1952                                                 hasAttachmentData.resolve();
1953                                         } else if ( control.params.attachment && control.params.attachment.id === value ) {
1954                                                 hasAttachmentData.resolve();
1955                                         }
1956                                 }
1957
1958                                 // Fetch the attachment data.
1959                                 if ( 'pending' === hasAttachmentData.state() ) {
1960                                         wp.media.attachment( value ).fetch().done( function() {
1961                                                 control.params.attachment = this.attributes;
1962                                                 hasAttachmentData.resolve();
1963
1964                                                 // Send attachment information to the preview for possible use in `postMessage` transport.
1965                                                 wp.customize.previewer.send( control.setting.id + '-attachment-data', this.attributes );
1966                                         } );
1967                                 }
1968
1969                                 hasAttachmentData.done( function() {
1970                                         control.renderContent();
1971                                 } );
1972                         }
1973
1974                         // Ensure attachment data is initially set (for dynamically-instantiated controls).
1975                         setAttachmentDataAndRenderContent( control.setting() );
1976
1977                         // Update the attachment data and re-render the control when the setting changes.
1978                         control.setting.bind( setAttachmentDataAndRenderContent );
1979                 },
1980
1981                 pausePlayer: function () {
1982                         this.player && this.player.pause();
1983                 },
1984
1985                 cleanupPlayer: function () {
1986                         this.player && wp.media.mixin.removePlayer( this.player );
1987                 },
1988
1989                 /**
1990                  * Open the media modal.
1991                  */
1992                 openFrame: function( event ) {
1993                         if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
1994                                 return;
1995                         }
1996
1997                         event.preventDefault();
1998
1999                         if ( ! this.frame ) {
2000                                 this.initFrame();
2001                         }
2002
2003                         this.frame.open();
2004                 },
2005
2006                 /**
2007                  * Create a media modal select frame, and store it so the instance can be reused when needed.
2008                  */
2009                 initFrame: function() {
2010                         this.frame = wp.media({
2011                                 button: {
2012                                         text: this.params.button_labels.frame_button
2013                                 },
2014                                 states: [
2015                                         new wp.media.controller.Library({
2016                                                 title:     this.params.button_labels.frame_title,
2017                                                 library:   wp.media.query({ type: this.params.mime_type }),
2018                                                 multiple:  false,
2019                                                 date:      false
2020                                         })
2021                                 ]
2022                         });
2023
2024                         // When a file is selected, run a callback.
2025                         this.frame.on( 'select', this.select );
2026                 },
2027
2028                 /**
2029                  * Callback handler for when an attachment is selected in the media modal.
2030                  * Gets the selected image information, and sets it within the control.
2031                  */
2032                 select: function() {
2033                         // Get the attachment from the modal frame.
2034                         var node,
2035                                 attachment = this.frame.state().get( 'selection' ).first().toJSON(),
2036                                 mejsSettings = window._wpmejsSettings || {};
2037
2038                         this.params.attachment = attachment;
2039
2040                         // Set the Customizer setting; the callback takes care of rendering.
2041                         this.setting( attachment.id );
2042                         node = this.container.find( 'audio, video' ).get(0);
2043
2044                         // Initialize audio/video previews.
2045                         if ( node ) {
2046                                 this.player = new MediaElementPlayer( node, mejsSettings );
2047                         } else {
2048                                 this.cleanupPlayer();
2049                         }
2050                 },
2051
2052                 /**
2053                  * Reset the setting to the default value.
2054                  */
2055                 restoreDefault: function( event ) {
2056                         if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2057                                 return;
2058                         }
2059                         event.preventDefault();
2060
2061                         this.params.attachment = this.params.defaultAttachment;
2062                         this.setting( this.params.defaultAttachment.url );
2063                 },
2064
2065                 /**
2066                  * Called when the "Remove" link is clicked. Empties the setting.
2067                  *
2068                  * @param {object} event jQuery Event object
2069                  */
2070                 removeFile: function( event ) {
2071                         if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2072                                 return;
2073                         }
2074                         event.preventDefault();
2075
2076                         this.params.attachment = {};
2077                         this.setting( '' );
2078                         this.renderContent(); // Not bound to setting change when emptying.
2079                 }
2080         });
2081
2082         /**
2083          * An upload control, which utilizes the media modal.
2084          *
2085          * @class
2086          * @augments wp.customize.MediaControl
2087          * @augments wp.customize.Control
2088          * @augments wp.customize.Class
2089          */
2090         api.UploadControl = api.MediaControl.extend({
2091
2092                 /**
2093                  * Callback handler for when an attachment is selected in the media modal.
2094                  * Gets the selected image information, and sets it within the control.
2095                  */
2096                 select: function() {
2097                         // Get the attachment from the modal frame.
2098                         var node,
2099                                 attachment = this.frame.state().get( 'selection' ).first().toJSON(),
2100                                 mejsSettings = window._wpmejsSettings || {};
2101
2102                         this.params.attachment = attachment;
2103
2104                         // Set the Customizer setting; the callback takes care of rendering.
2105                         this.setting( attachment.url );
2106                         node = this.container.find( 'audio, video' ).get(0);
2107
2108                         // Initialize audio/video previews.
2109                         if ( node ) {
2110                                 this.player = new MediaElementPlayer( node, mejsSettings );
2111                         } else {
2112                                 this.cleanupPlayer();
2113                         }
2114                 },
2115
2116                 // @deprecated
2117                 success: function() {},
2118
2119                 // @deprecated
2120                 removerVisibility: function() {}
2121         });
2122
2123         /**
2124          * A control for uploading images.
2125          *
2126          * This control no longer needs to do anything more
2127          * than what the upload control does in JS.
2128          *
2129          * @class
2130          * @augments wp.customize.UploadControl
2131          * @augments wp.customize.MediaControl
2132          * @augments wp.customize.Control
2133          * @augments wp.customize.Class
2134          */
2135         api.ImageControl = api.UploadControl.extend({
2136                 // @deprecated
2137                 thumbnailSrc: function() {}
2138         });
2139
2140         /**
2141          * A control for uploading background images.
2142          *
2143          * @class
2144          * @augments wp.customize.UploadControl
2145          * @augments wp.customize.MediaControl
2146          * @augments wp.customize.Control
2147          * @augments wp.customize.Class
2148          */
2149         api.BackgroundControl = api.UploadControl.extend({
2150
2151                 /**
2152                  * When the control's DOM structure is ready,
2153                  * set up internal event bindings.
2154                  */
2155                 ready: function() {
2156                         api.UploadControl.prototype.ready.apply( this, arguments );
2157                 },
2158
2159                 /**
2160                  * Callback handler for when an attachment is selected in the media modal.
2161                  * Does an additional AJAX request for setting the background context.
2162                  */
2163                 select: function() {
2164                         api.UploadControl.prototype.select.apply( this, arguments );
2165
2166                         wp.ajax.post( 'custom-background-add', {
2167                                 nonce: _wpCustomizeBackground.nonces.add,
2168                                 wp_customize: 'on',
2169                                 theme: api.settings.theme.stylesheet,
2170                                 attachment_id: this.params.attachment.id
2171                         } );
2172                 }
2173         });
2174
2175         /**
2176          * A control for selecting and cropping an image.
2177          *
2178          * @class
2179          * @augments wp.customize.MediaControl
2180          * @augments wp.customize.Control
2181          * @augments wp.customize.Class
2182          */
2183         api.CroppedImageControl = api.MediaControl.extend({
2184
2185                 /**
2186                  * Open the media modal to the library state.
2187                  */
2188                 openFrame: function( event ) {
2189                         if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2190                                 return;
2191                         }
2192
2193                         this.initFrame();
2194                         this.frame.setState( 'library' ).open();
2195                 },
2196
2197                 /**
2198                  * Create a media modal select frame, and store it so the instance can be reused when needed.
2199                  */
2200                 initFrame: function() {
2201                         var l10n = _wpMediaViewsL10n;
2202
2203                         this.frame = wp.media({
2204                                 button: {
2205                                         text: l10n.select,
2206                                         close: false
2207                                 },
2208                                 states: [
2209                                         new wp.media.controller.Library({
2210                                                 title: this.params.button_labels.frame_title,
2211                                                 library: wp.media.query({ type: 'image' }),
2212                                                 multiple: false,
2213                                                 date: false,
2214                                                 priority: 20,
2215                                                 suggestedWidth: this.params.width,
2216                                                 suggestedHeight: this.params.height
2217                                         }),
2218                                         new wp.media.controller.CustomizeImageCropper({
2219                                                 imgSelectOptions: this.calculateImageSelectOptions,
2220                                                 control: this
2221                                         })
2222                                 ]
2223                         });
2224
2225                         this.frame.on( 'select', this.onSelect, this );
2226                         this.frame.on( 'cropped', this.onCropped, this );
2227                         this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
2228                 },
2229
2230                 /**
2231                  * After an image is selected in the media modal, switch to the cropper
2232                  * state if the image isn't the right size.
2233                  */
2234                 onSelect: function() {
2235                         var attachment = this.frame.state().get( 'selection' ).first().toJSON();
2236
2237                         if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
2238                                 this.setImageFromAttachment( attachment );
2239                                 this.frame.close();
2240                         } else {
2241                                 this.frame.setState( 'cropper' );
2242                         }
2243                 },
2244
2245                 /**
2246                  * After the image has been cropped, apply the cropped image data to the setting.
2247                  *
2248                  * @param {object} croppedImage Cropped attachment data.
2249                  */
2250                 onCropped: function( croppedImage ) {
2251                         this.setImageFromAttachment( croppedImage );
2252                 },
2253
2254                 /**
2255                  * Returns a set of options, computed from the attached image data and
2256                  * control-specific data, to be fed to the imgAreaSelect plugin in
2257                  * wp.media.view.Cropper.
2258                  *
2259                  * @param {wp.media.model.Attachment} attachment
2260                  * @param {wp.media.controller.Cropper} controller
2261                  * @returns {Object} Options
2262                  */
2263                 calculateImageSelectOptions: function( attachment, controller ) {
2264                         var control    = controller.get( 'control' ),
2265                                 flexWidth  = !! parseInt( control.params.flex_width, 10 ),
2266                                 flexHeight = !! parseInt( control.params.flex_height, 10 ),
2267                                 realWidth  = attachment.get( 'width' ),
2268                                 realHeight = attachment.get( 'height' ),
2269                                 xInit = parseInt( control.params.width, 10 ),
2270                                 yInit = parseInt( control.params.height, 10 ),
2271                                 ratio = xInit / yInit,
2272                                 xImg  = xInit,
2273                                 yImg  = yInit,
2274                                 x1, y1, imgSelectOptions;
2275
2276                         controller.set( 'canSkipCrop', ! control.mustBeCropped( flexWidth, flexHeight, xInit, yInit, realWidth, realHeight ) );
2277
2278                         if ( realWidth / realHeight > ratio ) {
2279                                 yInit = realHeight;
2280                                 xInit = yInit * ratio;
2281                         } else {
2282                                 xInit = realWidth;
2283                                 yInit = xInit / ratio;
2284                         }
2285
2286                         x1 = ( realWidth - xInit ) / 2;
2287                         y1 = ( realHeight - yInit ) / 2;
2288
2289                         imgSelectOptions = {
2290                                 handles: true,
2291                                 keys: true,
2292                                 instance: true,
2293                                 persistent: true,
2294                                 imageWidth: realWidth,
2295                                 imageHeight: realHeight,
2296                                 minWidth: xImg > xInit ? xInit : xImg,
2297                                 minHeight: yImg > yInit ? yInit : yImg,
2298                                 x1: x1,
2299                                 y1: y1,
2300                                 x2: xInit + x1,
2301                                 y2: yInit + y1
2302                         };
2303
2304                         if ( flexHeight === false && flexWidth === false ) {
2305                                 imgSelectOptions.aspectRatio = xInit + ':' + yInit;
2306                         }
2307
2308                         if ( true === flexHeight ) {
2309                                 delete imgSelectOptions.minHeight;
2310                                 imgSelectOptions.maxWidth = realWidth;
2311                         }
2312
2313                         if ( true === flexWidth ) {
2314                                 delete imgSelectOptions.minWidth;
2315                                 imgSelectOptions.maxHeight = realHeight;
2316                         }
2317
2318                         return imgSelectOptions;
2319                 },
2320
2321                 /**
2322                  * Return whether the image must be cropped, based on required dimensions.
2323                  *
2324                  * @param {bool} flexW
2325                  * @param {bool} flexH
2326                  * @param {int}  dstW
2327                  * @param {int}  dstH
2328                  * @param {int}  imgW
2329                  * @param {int}  imgH
2330                  * @return {bool}
2331                  */
2332                 mustBeCropped: function( flexW, flexH, dstW, dstH, imgW, imgH ) {
2333                         if ( true === flexW && true === flexH ) {
2334                                 return false;
2335                         }
2336
2337                         if ( true === flexW && dstH === imgH ) {
2338                                 return false;
2339                         }
2340
2341                         if ( true === flexH && dstW === imgW ) {
2342                                 return false;
2343                         }
2344
2345                         if ( dstW === imgW && dstH === imgH ) {
2346                                 return false;
2347                         }
2348
2349                         if ( imgW <= dstW ) {
2350                                 return false;
2351                         }
2352
2353                         return true;
2354                 },
2355
2356                 /**
2357                  * If cropping was skipped, apply the image data directly to the setting.
2358                  */
2359                 onSkippedCrop: function() {
2360                         var attachment = this.frame.state().get( 'selection' ).first().toJSON();
2361                         this.setImageFromAttachment( attachment );
2362                 },
2363
2364                 /**
2365                  * Updates the setting and re-renders the control UI.
2366                  *
2367                  * @param {object} attachment
2368                  */
2369                 setImageFromAttachment: function( attachment ) {
2370                         this.params.attachment = attachment;
2371
2372                         // Set the Customizer setting; the callback takes care of rendering.
2373                         this.setting( attachment.id );
2374                 }
2375         });
2376
2377         /**
2378          * A control for selecting and cropping Site Icons.
2379          *
2380          * @class
2381          * @augments wp.customize.CroppedImageControl
2382          * @augments wp.customize.MediaControl
2383          * @augments wp.customize.Control
2384          * @augments wp.customize.Class
2385          */
2386         api.SiteIconControl = api.CroppedImageControl.extend({
2387
2388                 /**
2389                  * Create a media modal select frame, and store it so the instance can be reused when needed.
2390                  */
2391                 initFrame: function() {
2392                         var l10n = _wpMediaViewsL10n;
2393
2394                         this.frame = wp.media({
2395                                 button: {
2396                                         text: l10n.select,
2397                                         close: false
2398                                 },
2399                                 states: [
2400                                         new wp.media.controller.Library({
2401                                                 title: this.params.button_labels.frame_title,
2402                                                 library: wp.media.query({ type: 'image' }),
2403                                                 multiple: false,
2404                                                 date: false,
2405                                                 priority: 20,
2406                                                 suggestedWidth: this.params.width,
2407                                                 suggestedHeight: this.params.height
2408                                         }),
2409                                         new wp.media.controller.SiteIconCropper({
2410                                                 imgSelectOptions: this.calculateImageSelectOptions,
2411                                                 control: this
2412                                         })
2413                                 ]
2414                         });
2415
2416                         this.frame.on( 'select', this.onSelect, this );
2417                         this.frame.on( 'cropped', this.onCropped, this );
2418                         this.frame.on( 'skippedcrop', this.onSkippedCrop, this );
2419                 },
2420
2421                 /**
2422                  * After an image is selected in the media modal, switch to the cropper
2423                  * state if the image isn't the right size.
2424                  */
2425                 onSelect: function() {
2426                         var attachment = this.frame.state().get( 'selection' ).first().toJSON(),
2427                                 controller = this;
2428
2429                         if ( this.params.width === attachment.width && this.params.height === attachment.height && ! this.params.flex_width && ! this.params.flex_height ) {
2430                                 wp.ajax.post( 'crop-image', {
2431                                         nonce: attachment.nonces.edit,
2432                                         id: attachment.id,
2433                                         context: 'site-icon',
2434                                         cropDetails: {
2435                                                 x1: 0,
2436                                                 y1: 0,
2437                                                 width: this.params.width,
2438                                                 height: this.params.height,
2439                                                 dst_width: this.params.width,
2440                                                 dst_height: this.params.height
2441                                         }
2442                                 } ).done( function( croppedImage ) {
2443                                         controller.setImageFromAttachment( croppedImage );
2444                                         controller.frame.close();
2445                                 } ).fail( function() {
2446                                         controller.frame.trigger('content:error:crop');
2447                                 } );
2448                         } else {
2449                                 this.frame.setState( 'cropper' );
2450                         }
2451                 },
2452
2453                 /**
2454                  * Updates the setting and re-renders the control UI.
2455                  *
2456                  * @param {object} attachment
2457                  */
2458                 setImageFromAttachment: function( attachment ) {
2459                         var sizes = [ 'site_icon-32', 'thumbnail', 'full' ],
2460                                 icon;
2461
2462                         _.each( sizes, function( size ) {
2463                                 if ( ! icon && ! _.isUndefined ( attachment.sizes[ size ] ) ) {
2464                                         icon = attachment.sizes[ size ];
2465                                 }
2466                         } );
2467
2468                         this.params.attachment = attachment;
2469
2470                         // Set the Customizer setting; the callback takes care of rendering.
2471                         this.setting( attachment.id );
2472
2473                         // Update the icon in-browser.
2474                         $( 'link[sizes="32x32"]' ).attr( 'href', icon.url );
2475                 },
2476
2477                 /**
2478                  * Called when the "Remove" link is clicked. Empties the setting.
2479                  *
2480                  * @param {object} event jQuery Event object
2481                  */
2482                 removeFile: function( event ) {
2483                         if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2484                                 return;
2485                         }
2486                         event.preventDefault();
2487
2488                         this.params.attachment = {};
2489                         this.setting( '' );
2490                         this.renderContent(); // Not bound to setting change when emptying.
2491                         $( 'link[rel="icon"]' ).attr( 'href', '' );
2492                 }
2493         });
2494
2495         /**
2496          * @class
2497          * @augments wp.customize.Control
2498          * @augments wp.customize.Class
2499          */
2500         api.HeaderControl = api.Control.extend({
2501                 ready: function() {
2502                         this.btnRemove = $('#customize-control-header_image .actions .remove');
2503                         this.btnNew    = $('#customize-control-header_image .actions .new');
2504
2505                         _.bindAll(this, 'openMedia', 'removeImage');
2506
2507                         this.btnNew.on( 'click', this.openMedia );
2508                         this.btnRemove.on( 'click', this.removeImage );
2509
2510                         api.HeaderTool.currentHeader = this.getInitialHeaderImage();
2511
2512                         new api.HeaderTool.CurrentView({
2513                                 model: api.HeaderTool.currentHeader,
2514                                 el: '#customize-control-header_image .current .container'
2515                         });
2516
2517                         new api.HeaderTool.ChoiceListView({
2518                                 collection: api.HeaderTool.UploadsList = new api.HeaderTool.ChoiceList(),
2519                                 el: '#customize-control-header_image .choices .uploaded .list'
2520                         });
2521
2522                         new api.HeaderTool.ChoiceListView({
2523                                 collection: api.HeaderTool.DefaultsList = new api.HeaderTool.DefaultsList(),
2524                                 el: '#customize-control-header_image .choices .default .list'
2525                         });
2526
2527                         api.HeaderTool.combinedList = api.HeaderTool.CombinedList = new api.HeaderTool.CombinedList([
2528                                 api.HeaderTool.UploadsList,
2529                                 api.HeaderTool.DefaultsList
2530                         ]);
2531
2532                         // Ensure custom-header-crop Ajax requests bootstrap the Customizer to activate the previewed theme.
2533                         wp.media.controller.Cropper.prototype.defaults.doCropArgs.wp_customize = 'on';
2534                         wp.media.controller.Cropper.prototype.defaults.doCropArgs.theme = api.settings.theme.stylesheet;
2535                 },
2536
2537                 /**
2538                  * Returns a new instance of api.HeaderTool.ImageModel based on the currently
2539                  * saved header image (if any).
2540                  *
2541                  * @since 4.2.0
2542                  *
2543                  * @returns {Object} Options
2544                  */
2545                 getInitialHeaderImage: function() {
2546                         if ( ! api.get().header_image || ! api.get().header_image_data || _.contains( [ 'remove-header', 'random-default-image', 'random-uploaded-image' ], api.get().header_image ) ) {
2547                                 return new api.HeaderTool.ImageModel();
2548                         }
2549
2550                         // Get the matching uploaded image object.
2551                         var currentHeaderObject = _.find( _wpCustomizeHeader.uploads, function( imageObj ) {
2552                                 return ( imageObj.attachment_id === api.get().header_image_data.attachment_id );
2553                         } );
2554                         // Fall back to raw current header image.
2555                         if ( ! currentHeaderObject ) {
2556                                 currentHeaderObject = {
2557                                         url: api.get().header_image,
2558                                         thumbnail_url: api.get().header_image,
2559                                         attachment_id: api.get().header_image_data.attachment_id
2560                                 };
2561                         }
2562
2563                         return new api.HeaderTool.ImageModel({
2564                                 header: currentHeaderObject,
2565                                 choice: currentHeaderObject.url.split( '/' ).pop()
2566                         });
2567                 },
2568
2569                 /**
2570                  * Returns a set of options, computed from the attached image data and
2571                  * theme-specific data, to be fed to the imgAreaSelect plugin in
2572                  * wp.media.view.Cropper.
2573                  *
2574                  * @param {wp.media.model.Attachment} attachment
2575                  * @param {wp.media.controller.Cropper} controller
2576                  * @returns {Object} Options
2577                  */
2578                 calculateImageSelectOptions: function(attachment, controller) {
2579                         var xInit = parseInt(_wpCustomizeHeader.data.width, 10),
2580                                 yInit = parseInt(_wpCustomizeHeader.data.height, 10),
2581                                 flexWidth = !! parseInt(_wpCustomizeHeader.data['flex-width'], 10),
2582                                 flexHeight = !! parseInt(_wpCustomizeHeader.data['flex-height'], 10),
2583                                 ratio, xImg, yImg, realHeight, realWidth,
2584                                 imgSelectOptions;
2585
2586                         realWidth = attachment.get('width');
2587                         realHeight = attachment.get('height');
2588
2589                         this.headerImage = new api.HeaderTool.ImageModel();
2590                         this.headerImage.set({
2591                                 themeWidth: xInit,
2592                                 themeHeight: yInit,
2593                                 themeFlexWidth: flexWidth,
2594                                 themeFlexHeight: flexHeight,
2595                                 imageWidth: realWidth,
2596                                 imageHeight: realHeight
2597                         });
2598
2599                         controller.set( 'canSkipCrop', ! this.headerImage.shouldBeCropped() );
2600
2601                         ratio = xInit / yInit;
2602                         xImg = realWidth;
2603                         yImg = realHeight;
2604
2605                         if ( xImg / yImg > ratio ) {
2606                                 yInit = yImg;
2607                                 xInit = yInit * ratio;
2608                         } else {
2609                                 xInit = xImg;
2610                                 yInit = xInit / ratio;
2611                         }
2612
2613                         imgSelectOptions = {
2614                                 handles: true,
2615                                 keys: true,
2616                                 instance: true,
2617                                 persistent: true,
2618                                 imageWidth: realWidth,
2619                                 imageHeight: realHeight,
2620                                 x1: 0,
2621                                 y1: 0,
2622                                 x2: xInit,
2623                                 y2: yInit
2624                         };
2625
2626                         if (flexHeight === false && flexWidth === false) {
2627                                 imgSelectOptions.aspectRatio = xInit + ':' + yInit;
2628                         }
2629                         if (flexHeight === false ) {
2630                                 imgSelectOptions.maxHeight = yInit;
2631                         }
2632                         if (flexWidth === false ) {
2633                                 imgSelectOptions.maxWidth = xInit;
2634                         }
2635
2636                         return imgSelectOptions;
2637                 },
2638
2639                 /**
2640                  * Sets up and opens the Media Manager in order to select an image.
2641                  * Depending on both the size of the image and the properties of the
2642                  * current theme, a cropping step after selection may be required or
2643                  * skippable.
2644                  *
2645                  * @param {event} event
2646                  */
2647                 openMedia: function(event) {
2648                         var l10n = _wpMediaViewsL10n;
2649
2650                         event.preventDefault();
2651
2652                         this.frame = wp.media({
2653                                 button: {
2654                                         text: l10n.selectAndCrop,
2655                                         close: false
2656                                 },
2657                                 states: [
2658                                         new wp.media.controller.Library({
2659                                                 title:     l10n.chooseImage,
2660                                                 library:   wp.media.query({ type: 'image' }),
2661                                                 multiple:  false,
2662                                                 date:      false,
2663                                                 priority:  20,
2664                                                 suggestedWidth: _wpCustomizeHeader.data.width,
2665                                                 suggestedHeight: _wpCustomizeHeader.data.height
2666                                         }),
2667                                         new wp.media.controller.Cropper({
2668                                                 imgSelectOptions: this.calculateImageSelectOptions
2669                                         })
2670                                 ]
2671                         });
2672
2673                         this.frame.on('select', this.onSelect, this);
2674                         this.frame.on('cropped', this.onCropped, this);
2675                         this.frame.on('skippedcrop', this.onSkippedCrop, this);
2676
2677                         this.frame.open();
2678                 },
2679
2680                 /**
2681                  * After an image is selected in the media modal,
2682                  * switch to the cropper state.
2683                  */
2684                 onSelect: function() {
2685                         this.frame.setState('cropper');
2686                 },
2687
2688                 /**
2689                  * After the image has been cropped, apply the cropped image data to the setting.
2690                  *
2691                  * @param {object} croppedImage Cropped attachment data.
2692                  */
2693                 onCropped: function(croppedImage) {
2694                         var url = croppedImage.url,
2695                                 attachmentId = croppedImage.attachment_id,
2696                                 w = croppedImage.width,
2697                                 h = croppedImage.height;
2698                         this.setImageFromURL(url, attachmentId, w, h);
2699                 },
2700
2701                 /**
2702                  * If cropping was skipped, apply the image data directly to the setting.
2703                  *
2704                  * @param {object} selection
2705                  */
2706                 onSkippedCrop: function(selection) {
2707                         var url = selection.get('url'),
2708                                 w = selection.get('width'),
2709                                 h = selection.get('height');
2710                         this.setImageFromURL(url, selection.id, w, h);
2711                 },
2712
2713                 /**
2714                  * Creates a new wp.customize.HeaderTool.ImageModel from provided
2715                  * header image data and inserts it into the user-uploaded headers
2716                  * collection.
2717                  *
2718                  * @param {String} url
2719                  * @param {Number} attachmentId
2720                  * @param {Number} width
2721                  * @param {Number} height
2722                  */
2723                 setImageFromURL: function(url, attachmentId, width, height) {
2724                         var choice, data = {};
2725
2726                         data.url = url;
2727                         data.thumbnail_url = url;
2728                         data.timestamp = _.now();
2729
2730                         if (attachmentId) {
2731                                 data.attachment_id = attachmentId;
2732                         }
2733
2734                         if (width) {
2735                                 data.width = width;
2736                         }
2737
2738                         if (height) {
2739                                 data.height = height;
2740                         }
2741
2742                         choice = new api.HeaderTool.ImageModel({
2743                                 header: data,
2744                                 choice: url.split('/').pop()
2745                         });
2746                         api.HeaderTool.UploadsList.add(choice);
2747                         api.HeaderTool.currentHeader.set(choice.toJSON());
2748                         choice.save();
2749                         choice.importImage();
2750                 },
2751
2752                 /**
2753                  * Triggers the necessary events to deselect an image which was set as
2754                  * the currently selected one.
2755                  */
2756                 removeImage: function() {
2757                         api.HeaderTool.currentHeader.trigger('hide');
2758                         api.HeaderTool.CombinedList.trigger('control:removeImage');
2759                 }
2760
2761         });
2762
2763         /**
2764          * wp.customize.ThemeControl
2765          *
2766          * @constructor
2767          * @augments wp.customize.Control
2768          * @augments wp.customize.Class
2769          */
2770         api.ThemeControl = api.Control.extend({
2771
2772                 touchDrag: false,
2773                 isRendered: false,
2774
2775                 /**
2776                  * Defer rendering the theme control until the section is displayed.
2777                  *
2778                  * @since 4.2.0
2779                  */
2780                 renderContent: function () {
2781                         var control = this,
2782                                 renderContentArgs = arguments;
2783
2784                         api.section( control.section(), function( section ) {
2785                                 if ( section.expanded() ) {
2786                                         api.Control.prototype.renderContent.apply( control, renderContentArgs );
2787                                         control.isRendered = true;
2788                                 } else {
2789                                         section.expanded.bind( function( expanded ) {
2790                                                 if ( expanded && ! control.isRendered ) {
2791                                                         api.Control.prototype.renderContent.apply( control, renderContentArgs );
2792                                                         control.isRendered = true;
2793                                                 }
2794                                         } );
2795                                 }
2796                         } );
2797                 },
2798
2799                 /**
2800                  * @since 4.2.0
2801                  */
2802                 ready: function() {
2803                         var control = this;
2804
2805                         control.container.on( 'touchmove', '.theme', function() {
2806                                 control.touchDrag = true;
2807                         });
2808
2809                         // Bind details view trigger.
2810                         control.container.on( 'click keydown touchend', '.theme', function( event ) {
2811                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2812                                         return;
2813                                 }
2814
2815                                 // Bail if the user scrolled on a touch device.
2816                                 if ( control.touchDrag === true ) {
2817                                         return control.touchDrag = false;
2818                                 }
2819
2820                                 // Prevent the modal from showing when the user clicks the action button.
2821                                 if ( $( event.target ).is( '.theme-actions .button' ) ) {
2822                                         return;
2823                                 }
2824
2825                                 var previewUrl = $( this ).data( 'previewUrl' );
2826
2827                                 $( '.wp-full-overlay' ).addClass( 'customize-loading' );
2828
2829                                 window.parent.location = previewUrl;
2830                         });
2831
2832                         control.container.on( 'click keydown', '.theme-actions .theme-details', function( event ) {
2833                                 if ( api.utils.isKeydownButNotEnterEvent( event ) ) {
2834                                         return;
2835                                 }
2836
2837                                 event.preventDefault(); // Keep this AFTER the key filter above
2838
2839                                 api.section( control.section() ).showDetails( control.params.theme );
2840                         });
2841
2842                         control.container.on( 'render-screenshot', function() {
2843                                 var $screenshot = $( this ).find( 'img' ),
2844                                         source = $screenshot.data( 'src' );
2845
2846                                 if ( source ) {
2847                                         $screenshot.attr( 'src', source );
2848                                 }
2849                         });
2850                 },
2851
2852                 /**
2853                  * Show or hide the theme based on the presence of the term in the title, description, and author.
2854                  *
2855                  * @since 4.2.0
2856                  */
2857                 filter: function( term ) {
2858                         var control = this,
2859                                 haystack = control.params.theme.name + ' ' +
2860                                         control.params.theme.description + ' ' +
2861                                         control.params.theme.tags + ' ' +
2862                                         control.params.theme.author;
2863                         haystack = haystack.toLowerCase().replace( '-', ' ' );
2864                         if ( -1 !== haystack.search( term ) ) {
2865                                 control.activate();
2866                         } else {
2867                                 control.deactivate();
2868                         }
2869                 }
2870         });
2871
2872         // Change objects contained within the main customize object to Settings.
2873         api.defaultConstructor = api.Setting;
2874
2875         // Create the collections for Controls, Sections and Panels.
2876         api.control = new api.Values({ defaultConstructor: api.Control });
2877         api.section = new api.Values({ defaultConstructor: api.Section });
2878         api.panel = new api.Values({ defaultConstructor: api.Panel });
2879
2880         /**
2881          * An object that fetches a preview in the background of the document, which
2882          * allows for seamless replacement of an existing preview.
2883          *
2884          * @class
2885          * @augments wp.customize.Messenger
2886          * @augments wp.customize.Class
2887          * @mixes wp.customize.Events
2888          */
2889         api.PreviewFrame = api.Messenger.extend({
2890                 sensitivity: 2000,
2891
2892                 /**
2893                  * Initialize the PreviewFrame.
2894                  *
2895                  * @param {object} params.container
2896                  * @param {object} params.signature
2897                  * @param {object} params.previewUrl
2898                  * @param {object} params.query
2899                  * @param {object} options
2900                  */
2901                 initialize: function( params, options ) {
2902                         var deferred = $.Deferred();
2903
2904                         /*
2905                          * Make the instance of the PreviewFrame the promise object
2906                          * so other objects can easily interact with it.
2907                          */
2908                         deferred.promise( this );
2909
2910                         this.container = params.container;
2911                         this.signature = params.signature;
2912
2913                         $.extend( params, { channel: api.PreviewFrame.uuid() });
2914
2915                         api.Messenger.prototype.initialize.call( this, params, options );
2916
2917                         this.add( 'previewUrl', params.previewUrl );
2918
2919                         this.query = $.extend( params.query || {}, { customize_messenger_channel: this.channel() });
2920
2921                         this.run( deferred );
2922                 },
2923
2924                 /**
2925                  * Run the preview request.
2926                  *
2927                  * @param {object} deferred jQuery Deferred object to be resolved with
2928                  *                          the request.
2929                  */
2930                 run: function( deferred ) {
2931                         var self   = this,
2932                                 loaded = false,
2933                                 ready  = false;
2934
2935                         if ( this._ready ) {
2936                                 this.unbind( 'ready', this._ready );
2937                         }
2938
2939                         this._ready = function() {
2940                                 ready = true;
2941
2942                                 if ( loaded ) {
2943                                         deferred.resolveWith( self );
2944                                 }
2945                         };
2946
2947                         this.bind( 'ready', this._ready );
2948
2949                         this.bind( 'ready', function ( data ) {
2950
2951                                 this.container.addClass( 'iframe-ready' );
2952
2953                                 if ( ! data ) {
2954                                         return;
2955                                 }
2956
2957                                 /*
2958                                  * Walk over all panels, sections, and controls and set their
2959                                  * respective active states to true if the preview explicitly
2960                                  * indicates as such.
2961                                  */
2962                                 var constructs = {
2963                                         panel: data.activePanels,
2964                                         section: data.activeSections,
2965                                         control: data.activeControls
2966                                 };
2967                                 _( constructs ).each( function ( activeConstructs, type ) {
2968                                         api[ type ].each( function ( construct, id ) {
2969                                                 var active = !! ( activeConstructs && activeConstructs[ id ] );
2970                                                 if ( active ) {
2971                                                         construct.activate();
2972                                                 } else {
2973                                                         construct.deactivate();
2974                                                 }
2975                                         } );
2976                                 } );
2977
2978                                 if ( data.settingValidities ) {
2979                                         api._handleSettingValidities( {
2980                                                 settingValidities: data.settingValidities,
2981                                                 focusInvalidControl: false
2982                                         } );
2983                                 }
2984                         } );
2985
2986                         this.request = $.ajax( this.previewUrl(), {
2987                                 type: 'POST',
2988                                 data: this.query,
2989                                 xhrFields: {
2990                                         withCredentials: true
2991                                 }
2992                         } );
2993
2994                         this.request.fail( function() {
2995                                 deferred.rejectWith( self, [ 'request failure' ] );
2996                         });
2997
2998                         this.request.done( function( response ) {
2999                                 var location = self.request.getResponseHeader('Location'),
3000                                         signature = self.signature,
3001                                         index;
3002
3003                                 // Check if the location response header differs from the current URL.
3004                                 // If so, the request was redirected; try loading the requested page.
3005                                 if ( location && location !== self.previewUrl() ) {
3006                                         deferred.rejectWith( self, [ 'redirect', location ] );
3007                                         return;
3008                                 }
3009
3010                                 // Check if the user is not logged in.
3011                                 if ( '0' === response ) {
3012                                         self.login( deferred );
3013                                         return;
3014                                 }
3015
3016                                 // Check for cheaters.
3017                                 if ( '-1' === response ) {
3018                                         deferred.rejectWith( self, [ 'cheatin' ] );
3019                                         return;
3020                                 }
3021
3022                                 // Check for a signature in the request.
3023                                 index = response.lastIndexOf( signature );
3024                                 if ( -1 === index || index < response.lastIndexOf('</html>') ) {
3025                                         deferred.rejectWith( self, [ 'unsigned' ] );
3026                                         return;
3027                                 }
3028
3029                                 // Strip the signature from the request.
3030                                 response = response.slice( 0, index ) + response.slice( index + signature.length );
3031
3032                                 // Create the iframe and inject the html content.
3033                                 self.iframe = $( '<iframe />', { 'title': api.l10n.previewIframeTitle } ).appendTo( self.container );
3034
3035                                 // Bind load event after the iframe has been added to the page;
3036                                 // otherwise it will fire when injected into the DOM.
3037                                 self.iframe.one( 'load', function() {
3038                                         loaded = true;
3039
3040                                         if ( ready ) {
3041                                                 deferred.resolveWith( self );
3042                                         } else {
3043                                                 setTimeout( function() {
3044                                                         deferred.rejectWith( self, [ 'ready timeout' ] );
3045                                                 }, self.sensitivity );
3046                                         }
3047                                 });
3048
3049                                 self.targetWindow( self.iframe[0].contentWindow );
3050
3051                                 self.targetWindow().document.open();
3052                                 self.targetWindow().document.write( response );
3053                                 self.targetWindow().document.close();
3054                         });
3055                 },
3056
3057                 login: function( deferred ) {
3058                         var self = this,
3059                                 reject;
3060
3061                         reject = function() {
3062                                 deferred.rejectWith( self, [ 'logged out' ] );
3063                         };
3064
3065                         if ( this.triedLogin ) {
3066                                 return reject();
3067                         }
3068
3069                         // Check if we have an admin cookie.
3070                         $.get( api.settings.url.ajax, {
3071                                 action: 'logged-in'
3072                         }).fail( reject ).done( function( response ) {
3073                                 var iframe;
3074
3075                                 if ( '1' !== response ) {
3076                                         reject();
3077                                 }
3078
3079                                 iframe = $( '<iframe />', { 'src': self.previewUrl(), 'title': api.l10n.previewIframeTitle } ).hide();
3080                                 iframe.appendTo( self.container );
3081                                 iframe.on( 'load', function() {
3082                                         self.triedLogin = true;
3083
3084                                         iframe.remove();
3085                                         self.run( deferred );
3086                                 });
3087                         });
3088                 },
3089
3090                 destroy: function() {
3091                         api.Messenger.prototype.destroy.call( this );
3092                         this.request.abort();
3093
3094                         if ( this.iframe )
3095                                 this.iframe.remove();
3096
3097                         delete this.request;
3098                         delete this.iframe;
3099                         delete this.targetWindow;
3100                 }
3101         });
3102
3103         (function(){
3104                 var uuid = 0;
3105                 /**
3106                  * Create a universally unique identifier.
3107                  *
3108                  * @return {int}
3109                  */
3110                 api.PreviewFrame.uuid = function() {
3111                         return 'preview-' + uuid++;
3112                 };
3113         }());
3114
3115         /**
3116          * Set the document title of the customizer.
3117          *
3118          * @since 4.1.0
3119          *
3120          * @param {string} documentTitle
3121          */
3122         api.setDocumentTitle = function ( documentTitle ) {
3123                 var tmpl, title;
3124                 tmpl = api.settings.documentTitleTmpl;
3125                 title = tmpl.replace( '%s', documentTitle );
3126                 document.title = title;
3127                 api.trigger( 'title', title );
3128         };
3129
3130         /**
3131          * @class
3132          * @augments wp.customize.Messenger
3133          * @augments wp.customize.Class
3134          * @mixes wp.customize.Events
3135          */
3136         api.Previewer = api.Messenger.extend({
3137                 refreshBuffer: 250,
3138
3139                 /**
3140                  * @param {array}  params.allowedUrls
3141                  * @param {string} params.container   A selector or jQuery element for the preview
3142                  *                                    frame to be placed.
3143                  * @param {string} params.form
3144                  * @param {string} params.previewUrl  The URL to preview.
3145                  * @param {string} params.signature
3146                  * @param {object} options
3147                  */
3148                 initialize: function( params, options ) {
3149                         var self = this,
3150                                 rscheme = /^https?/;
3151
3152                         $.extend( this, options || {} );
3153                         this.deferred = {
3154                                 active: $.Deferred()
3155                         };
3156
3157                         /*
3158                          * Wrap this.refresh to prevent it from hammering the servers:
3159                          *
3160                          * If refresh is called once and no other refresh requests are
3161                          * loading, trigger the request immediately.
3162                          *
3163                          * If refresh is called while another refresh request is loading,
3164                          * debounce the refresh requests:
3165                          * 1. Stop the loading request (as it is instantly outdated).
3166                          * 2. Trigger the new request once refresh hasn't been called for
3167                          *    self.refreshBuffer milliseconds.
3168                          */
3169                         this.refresh = (function( self ) {
3170                                 var refresh  = self.refresh,
3171                                         callback = function() {
3172                                                 timeout = null;
3173                                                 refresh.call( self );
3174                                         },
3175                                         timeout;
3176
3177                                 return function() {
3178                                         if ( typeof timeout !== 'number' ) {
3179                                                 if ( self.loading ) {
3180                                                         self.abort();
3181                                                 } else {
3182                                                         return callback();
3183                                                 }
3184                                         }
3185
3186                                         clearTimeout( timeout );
3187                                         timeout = setTimeout( callback, self.refreshBuffer );
3188                                 };
3189                         })( this );
3190
3191                         this.container   = api.ensure( params.container );
3192                         this.allowedUrls = params.allowedUrls;
3193                         this.signature   = params.signature;
3194
3195                         params.url = window.location.href;
3196
3197                         api.Messenger.prototype.initialize.call( this, params );
3198
3199                         this.add( 'scheme', this.origin() ).link( this.origin ).setter( function( to ) {
3200                                 var match = to.match( rscheme );
3201                                 return match ? match[0] : '';
3202                         });
3203
3204                         // Limit the URL to internal, front-end links.
3205                         //
3206                         // If the front end and the admin are served from the same domain, load the
3207                         // preview over ssl if the Customizer is being loaded over ssl. This avoids
3208                         // insecure content warnings. This is not attempted if the admin and front end
3209                         // are on different domains to avoid the case where the front end doesn't have
3210                         // ssl certs.
3211
3212                         this.add( 'previewUrl', params.previewUrl ).setter( function( to ) {
3213                                 var result;
3214
3215                                 // Check for URLs that include "/wp-admin/" or end in "/wp-admin".
3216                                 // Strip hashes and query strings before testing.
3217                                 if ( /\/wp-admin(\/|$)/.test( to.replace( /[#?].*$/, '' ) ) )
3218                                         return null;
3219
3220                                 // Attempt to match the URL to the control frame's scheme
3221                                 // and check if it's allowed. If not, try the original URL.
3222                                 $.each([ to.replace( rscheme, self.scheme() ), to ], function( i, url ) {
3223                                         $.each( self.allowedUrls, function( i, allowed ) {
3224                                                 var path;
3225
3226                                                 allowed = allowed.replace( /\/+$/, '' );
3227                                                 path = url.replace( allowed, '' );
3228
3229                                                 if ( 0 === url.indexOf( allowed ) && /^([/#?]|$)/.test( path ) ) {
3230                                                         result = url;
3231                                                         return false;
3232                                                 }
3233                                         });
3234                                         if ( result )
3235                                                 return false;
3236                                 });
3237
3238                                 // If we found a matching result, return it. If not, bail.
3239                                 return result ? result : null;
3240                         });
3241
3242                         // Refresh the preview when the URL is changed (but not yet).
3243                         this.previewUrl.bind( this.refresh );
3244
3245                         this.scroll = 0;
3246                         this.bind( 'scroll', function( distance ) {
3247                                 this.scroll = distance;
3248                         });
3249
3250                         // Update the URL when the iframe sends a URL message.
3251                         this.bind( 'url', this.previewUrl );
3252
3253                         // Update the document title when the preview changes.
3254                         this.bind( 'documentTitle', function ( title ) {
3255                                 api.setDocumentTitle( title );
3256                         } );
3257                 },
3258
3259                 /**
3260                  * Query string data sent with each preview request.
3261                  *
3262                  * @abstract
3263                  */
3264                 query: function() {},
3265
3266                 abort: function() {
3267                         if ( this.loading ) {
3268                                 this.loading.destroy();
3269                                 delete this.loading;
3270                         }
3271                 },
3272
3273                 /**
3274                  * Refresh the preview.
3275                  */
3276                 refresh: function() {
3277                         var self = this;
3278
3279                         // Display loading indicator
3280                         this.send( 'loading-initiated' );
3281
3282                         this.abort();
3283
3284                         this.loading = new api.PreviewFrame({
3285                                 url:        this.url(),
3286                                 previewUrl: this.previewUrl(),
3287                                 query:      this.query() || {},
3288                                 container:  this.container,
3289                                 signature:  this.signature
3290                         });
3291
3292                         this.loading.done( function() {
3293                                 // 'this' is the loading frame
3294                                 this.bind( 'synced', function() {
3295                                         if ( self.preview )
3296                                                 self.preview.destroy();
3297                                         self.preview = this;
3298                                         delete self.loading;
3299
3300                                         self.targetWindow( this.targetWindow() );
3301                                         self.channel( this.channel() );
3302
3303                                         self.deferred.active.resolve();
3304                                         self.send( 'active' );
3305                                 });
3306
3307                                 this.send( 'sync', {
3308                                         scroll:   self.scroll,
3309                                         settings: api.get()
3310                                 });
3311                         });
3312
3313                         this.loading.fail( function( reason, location ) {
3314                                 self.send( 'loading-failed' );
3315                                 if ( 'redirect' === reason && location ) {
3316                                         self.previewUrl( location );
3317                                 }
3318
3319                                 if ( 'logged out' === reason ) {
3320                                         if ( self.preview ) {
3321                                                 self.preview.destroy();
3322                                                 delete self.preview;
3323                                         }
3324
3325                                         self.login().done( self.refresh );
3326                                 }
3327
3328                                 if ( 'cheatin' === reason ) {
3329                                         self.cheatin();
3330                                 }
3331                         });
3332                 },
3333
3334                 login: function() {
3335                         var previewer = this,
3336                                 deferred, messenger, iframe;
3337
3338                         if ( this._login )
3339                                 return this._login;
3340
3341                         deferred = $.Deferred();
3342                         this._login = deferred.promise();
3343
3344                         messenger = new api.Messenger({
3345                                 channel: 'login',
3346                                 url:     api.settings.url.login
3347                         });
3348
3349                         iframe = $( '<iframe />', { 'src': api.settings.url.login, 'title': api.l10n.loginIframeTitle } ).appendTo( this.container );
3350
3351                         messenger.targetWindow( iframe[0].contentWindow );
3352
3353                         messenger.bind( 'login', function () {
3354                                 var refreshNonces = previewer.refreshNonces();
3355
3356                                 refreshNonces.always( function() {
3357                                         iframe.remove();
3358                                         messenger.destroy();
3359                                         delete previewer._login;
3360                                 });
3361
3362                                 refreshNonces.done( function() {
3363                                         deferred.resolve();
3364                                 });
3365
3366                                 refreshNonces.fail( function() {
3367                                         previewer.cheatin();
3368                                         deferred.reject();
3369                                 });
3370                         });
3371
3372                         return this._login;
3373                 },
3374
3375                 cheatin: function() {
3376                         $( document.body ).empty().addClass( 'cheatin' ).append(
3377                                 '<h1>' + api.l10n.cheatin + '</h1>' +
3378                                 '<p>' + api.l10n.notAllowed + '</p>'
3379                         );
3380                 },
3381
3382                 refreshNonces: function() {
3383                         var request, deferred = $.Deferred();
3384
3385                         deferred.promise();
3386
3387                         request = wp.ajax.post( 'customize_refresh_nonces', {
3388                                 wp_customize: 'on',
3389                                 theme: api.settings.theme.stylesheet
3390                         });
3391
3392                         request.done( function( response ) {
3393                                 api.trigger( 'nonce-refresh', response );
3394                                 deferred.resolve();
3395                         });
3396
3397                         request.fail( function() {
3398                                 deferred.reject();
3399                         });
3400
3401                         return deferred;
3402                 }
3403         });
3404
3405         api.settingConstructor = {};
3406         api.controlConstructor = {
3407                 color:         api.ColorControl,
3408                 media:         api.MediaControl,
3409                 upload:        api.UploadControl,
3410                 image:         api.ImageControl,
3411                 cropped_image: api.CroppedImageControl,
3412                 site_icon:     api.SiteIconControl,
3413                 header:        api.HeaderControl,
3414                 background:    api.BackgroundControl,
3415                 theme:         api.ThemeControl
3416         };
3417         api.panelConstructor = {};
3418         api.sectionConstructor = {
3419                 themes: api.ThemesSection
3420         };
3421
3422         /**
3423          * Handle setting_validities in an error response for the customize-save request.
3424          *
3425          * Add notifications to the settings and focus on the first control that has an invalid setting.
3426          *
3427          * @since 4.6.0
3428          * @private
3429          *
3430          * @param {object}  args
3431          * @param {object}  args.settingValidities
3432          * @param {boolean} [args.focusInvalidControl=false]
3433          * @returns {void}
3434          */
3435         api._handleSettingValidities = function handleSettingValidities( args ) {
3436                 var invalidSettingControls, invalidSettings = [], wasFocused = false;
3437
3438                 // Find the controls that correspond to each invalid setting.
3439                 _.each( args.settingValidities, function( validity, settingId ) {
3440                         var setting = api( settingId );
3441                         if ( setting ) {
3442
3443                                 // Add notifications for invalidities.
3444                                 if ( _.isObject( validity ) ) {
3445                                         _.each( validity, function( params, code ) {
3446                                                 var notification = new api.Notification( code, params ), existingNotification, needsReplacement = false;
3447
3448                                                 // Remove existing notification if already exists for code but differs in parameters.
3449                                                 existingNotification = setting.notifications( notification.code );
3450                                                 if ( existingNotification ) {
3451                                                         needsReplacement = ( notification.type !== existingNotification.type ) || ! _.isEqual( notification.data, existingNotification.data );
3452                                                 }
3453                                                 if ( needsReplacement ) {
3454                                                         setting.notifications.remove( code );
3455                                                 }
3456
3457                                                 if ( ! setting.notifications.has( notification.code ) ) {
3458                                                         setting.notifications.add( code, notification );
3459                                                 }
3460                                                 invalidSettings.push( setting.id );
3461                                         } );
3462                                 }
3463
3464                                 // Remove notification errors that are no longer valid.
3465                                 setting.notifications.each( function( notification ) {
3466                                         if ( 'error' === notification.type && ( true === validity || ! validity[ notification.code ] ) ) {
3467                                                 setting.notifications.remove( notification.code );
3468                                         }
3469                                 } );
3470                         }
3471                 } );
3472
3473                 if ( args.focusInvalidControl ) {
3474                         invalidSettingControls = api.findControlsForSettings( invalidSettings );
3475
3476                         // Focus on the first control that is inside of an expanded section (one that is visible).
3477                         _( _.values( invalidSettingControls ) ).find( function( controls ) {
3478                                 return _( controls ).find( function( control ) {
3479                                         var isExpanded = control.section() && api.section.has( control.section() ) && api.section( control.section() ).expanded();
3480                                         if ( isExpanded && control.expanded ) {
3481                                                 isExpanded = control.expanded();
3482                                         }
3483                                         if ( isExpanded ) {
3484                                                 control.focus();
3485                                                 wasFocused = true;
3486                                         }
3487                                         return wasFocused;
3488                                 } );
3489                         } );
3490
3491                         // Focus on the first invalid control.
3492                         if ( ! wasFocused && ! _.isEmpty( invalidSettingControls ) ) {
3493                                 _.values( invalidSettingControls )[0][0].focus();
3494                         }
3495                 }
3496         };
3497
3498         /**
3499          * Find all controls associated with the given settings.
3500          *
3501          * @since 4.6.0
3502          * @param {string[]} settingIds Setting IDs.
3503          * @returns {object<string, wp.customize.Control>} Mapping setting ids to arrays of controls.
3504          */
3505         api.findControlsForSettings = function findControlsForSettings( settingIds ) {
3506                 var controls = {}, settingControls;
3507                 _.each( _.unique( settingIds ), function( settingId ) {
3508                         var setting = api( settingId );
3509                         if ( setting ) {
3510                                 settingControls = setting.findControls();
3511                                 if ( settingControls && settingControls.length > 0 ) {
3512                                         controls[ settingId ] = settingControls;
3513                                 }
3514                         }
3515                 } );
3516                 return controls;
3517         };
3518
3519         /**
3520          * Sort panels, sections, controls by priorities. Hide empty sections and panels.
3521          *
3522          * @since 4.1.0
3523          */
3524         api.reflowPaneContents = _.bind( function () {
3525
3526                 var appendContainer, activeElement, rootContainers, rootNodes = [], wasReflowed = false;
3527
3528                 if ( document.activeElement ) {
3529                         activeElement = $( document.activeElement );
3530                 }
3531
3532                 // Sort the sections within each panel
3533                 api.panel.each( function ( panel ) {
3534                         var sections = panel.sections(),
3535                                 sectionContainers = _.pluck( sections, 'container' );
3536                         rootNodes.push( panel );
3537                         appendContainer = panel.container.find( 'ul:first' );
3538                         if ( ! api.utils.areElementListsEqual( sectionContainers, appendContainer.children( '[id]' ) ) ) {
3539                                 _( sections ).each( function ( section ) {
3540                                         appendContainer.append( section.container );
3541                                 } );
3542                                 wasReflowed = true;
3543                         }
3544                 } );
3545
3546                 // Sort the controls within each section
3547                 api.section.each( function ( section ) {
3548                         var controls = section.controls(),
3549                                 controlContainers = _.pluck( controls, 'container' );
3550                         if ( ! section.panel() ) {
3551                                 rootNodes.push( section );
3552                         }
3553                         appendContainer = section.container.find( 'ul:first' );
3554                         if ( ! api.utils.areElementListsEqual( controlContainers, appendContainer.children( '[id]' ) ) ) {
3555                                 _( controls ).each( function ( control ) {
3556                                         appendContainer.append( control.container );
3557                                 } );
3558                                 wasReflowed = true;
3559                         }
3560                 } );
3561
3562                 // Sort the root panels and sections
3563                 rootNodes.sort( api.utils.prioritySort );
3564                 rootContainers = _.pluck( rootNodes, 'container' );
3565                 appendContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable
3566                 if ( ! api.utils.areElementListsEqual( rootContainers, appendContainer.children() ) ) {
3567                         _( rootNodes ).each( function ( rootNode ) {
3568                                 appendContainer.append( rootNode.container );
3569                         } );
3570                         wasReflowed = true;
3571                 }
3572
3573                 // Now re-trigger the active Value callbacks to that the panels and sections can decide whether they can be rendered
3574                 api.panel.each( function ( panel ) {
3575                         var value = panel.active();
3576                         panel.active.callbacks.fireWith( panel.active, [ value, value ] );
3577                 } );
3578                 api.section.each( function ( section ) {
3579                         var value = section.active();
3580                         section.active.callbacks.fireWith( section.active, [ value, value ] );
3581                 } );
3582
3583                 // Restore focus if there was a reflow and there was an active (focused) element
3584                 if ( wasReflowed && activeElement ) {
3585                         activeElement.focus();
3586                 }
3587                 api.trigger( 'pane-contents-reflowed' );
3588         }, api );
3589
3590         $( function() {
3591                 api.settings = window._wpCustomizeSettings;
3592                 api.l10n = window._wpCustomizeControlsL10n;
3593
3594                 // Check if we can run the Customizer.
3595                 if ( ! api.settings ) {
3596                         return;
3597                 }
3598
3599                 // Bail if any incompatibilities are found.
3600                 if ( ! $.support.postMessage || ( ! $.support.cors && api.settings.isCrossDomain ) ) {
3601                         return;
3602                 }
3603
3604                 var parent, topFocus,
3605                         body = $( document.body ),
3606                         overlay = body.children( '.wp-full-overlay' ),
3607                         title = $( '#customize-info .panel-title.site-title' ),
3608                         closeBtn = $( '.customize-controls-close' ),
3609                         saveBtn = $( '#save' ),
3610                         footerActions = $( '#customize-footer-actions' );
3611
3612                 // Prevent the form from saving when enter is pressed on an input or select element.
3613                 $('#customize-controls').on( 'keydown', function( e ) {
3614                         var isEnter = ( 13 === e.which ),
3615                                 $el = $( e.target );
3616
3617                         if ( isEnter && ( $el.is( 'input:not([type=button])' ) || $el.is( 'select' ) ) ) {
3618                                 e.preventDefault();
3619                         }
3620                 });
3621
3622                 // Expand/Collapse the main customizer customize info.
3623                 $( '.customize-info' ).find( '> .accordion-section-title .customize-help-toggle' ).on( 'click', function() {
3624                         var section = $( this ).closest( '.accordion-section' ),
3625                                 content = section.find( '.customize-panel-description:first' );
3626
3627                         if ( section.hasClass( 'cannot-expand' ) ) {
3628                                 return;
3629                         }
3630
3631                         if ( section.hasClass( 'open' ) ) {
3632                                 section.toggleClass( 'open' );
3633                                 content.slideUp( api.Panel.prototype.defaultExpandedArguments.duration );
3634                                 $( this ).attr( 'aria-expanded', false );
3635                         } else {
3636                                 content.slideDown( api.Panel.prototype.defaultExpandedArguments.duration );
3637                                 section.toggleClass( 'open' );
3638                                 $( this ).attr( 'aria-expanded', true );
3639                         }
3640                 });
3641
3642                 // Initialize Previewer
3643                 api.previewer = new api.Previewer({
3644                         container:   '#customize-preview',
3645                         form:        '#customize-controls',
3646                         previewUrl:  api.settings.url.preview,
3647                         allowedUrls: api.settings.url.allowed,
3648                         signature:   'WP_CUSTOMIZER_SIGNATURE'
3649                 }, {
3650
3651                         nonce: api.settings.nonce,
3652
3653                         /**
3654                          * Build the query to send along with the Preview request.
3655                          *
3656                          * @return {object}
3657                          */
3658                         query: function() {
3659                                 var dirtyCustomized = {};
3660                                 api.each( function ( value, key ) {
3661                                         if ( value._dirty ) {
3662                                                 dirtyCustomized[ key ] = value();
3663                                         }
3664                                 } );
3665
3666                                 return {
3667                                         wp_customize: 'on',
3668                                         theme:      api.settings.theme.stylesheet,
3669                                         customized: JSON.stringify( dirtyCustomized ),
3670                                         nonce:      this.nonce.preview
3671                                 };
3672                         },
3673
3674                         save: function() {
3675                                 var self = this,
3676                                         processing = api.state( 'processing' ),
3677                                         submitWhenDoneProcessing,
3678                                         submit,
3679                                         modifiedWhileSaving = {},
3680                                         invalidSettings = [],
3681                                         invalidControls;
3682
3683                                 body.addClass( 'saving' );
3684
3685                                 function captureSettingModifiedDuringSave( setting ) {
3686                                         modifiedWhileSaving[ setting.id ] = true;
3687                                 }
3688                                 api.bind( 'change', captureSettingModifiedDuringSave );
3689
3690                                 submit = function () {
3691                                         var request, query;
3692
3693                                         /*
3694                                          * Block saving if there are any settings that are marked as
3695                                          * invalid from the client (not from the server). Focus on
3696                                          * the control.
3697                                          */
3698                                         api.each( function( setting ) {
3699                                                 setting.notifications.each( function( notification ) {
3700                                                         if ( 'error' === notification.type && ( ! notification.data || ! notification.data.from_server ) ) {
3701                                                                 invalidSettings.push( setting.id );
3702                                                         }
3703                                                 } );
3704                                         } );
3705                                         invalidControls = api.findControlsForSettings( invalidSettings );
3706                                         if ( ! _.isEmpty( invalidControls ) ) {
3707                                                 _.values( invalidControls )[0][0].focus();
3708                                                 body.removeClass( 'saving' );
3709                                                 api.unbind( 'change', captureSettingModifiedDuringSave );
3710                                                 return;
3711                                         }
3712
3713                                         query = $.extend( self.query(), {
3714                                                 nonce:  self.nonce.save
3715                                         } );
3716                                         request = wp.ajax.post( 'customize_save', query );
3717
3718                                         // Disable save button during the save request.
3719                                         saveBtn.prop( 'disabled', true );
3720
3721                                         api.trigger( 'save', request );
3722
3723                                         request.always( function () {
3724                                                 body.removeClass( 'saving' );
3725                                                 saveBtn.prop( 'disabled', false );
3726                                                 api.unbind( 'change', captureSettingModifiedDuringSave );
3727                                         } );
3728
3729                                         request.fail( function ( response ) {
3730                                                 if ( '0' === response ) {
3731                                                         response = 'not_logged_in';
3732                                                 } else if ( '-1' === response ) {
3733                                                         // Back-compat in case any other check_ajax_referer() call is dying
3734                                                         response = 'invalid_nonce';
3735                                                 }
3736
3737                                                 if ( 'invalid_nonce' === response ) {
3738                                                         self.cheatin();
3739                                                 } else if ( 'not_logged_in' === response ) {
3740                                                         self.preview.iframe.hide();
3741                                                         self.login().done( function() {
3742                                                                 self.save();
3743                                                                 self.preview.iframe.show();
3744                                                         } );
3745                                                 }
3746
3747                                                 if ( response.setting_validities ) {
3748                                                         api._handleSettingValidities( {
3749                                                                 settingValidities: response.setting_validities,
3750                                                                 focusInvalidControl: true
3751                                                         } );
3752                                                 }
3753
3754                                                 api.trigger( 'error', response );
3755                                         } );
3756
3757                                         request.done( function( response ) {
3758
3759                                                 // Clear setting dirty states, if setting wasn't modified while saving.
3760                                                 api.each( function( setting ) {
3761                                                         if ( ! modifiedWhileSaving[ setting.id ] ) {
3762                                                                 setting._dirty = false;
3763                                                         }
3764                                                 } );
3765
3766                                                 api.previewer.send( 'saved', response );
3767
3768                                                 if ( response.setting_validities ) {
3769                                                         api._handleSettingValidities( {
3770                                                                 settingValidities: response.setting_validities,
3771                                                                 focusInvalidControl: true
3772                                                         } );
3773                                                 }
3774
3775                                                 api.trigger( 'saved', response );
3776
3777                                                 // Restore the global dirty state if any settings were modified during save.
3778                                                 if ( ! _.isEmpty( modifiedWhileSaving ) ) {
3779                                                         api.state( 'saved' ).set( false );
3780                                                 }
3781                                         } );
3782                                 };
3783
3784                                 if ( 0 === processing() ) {
3785                                         submit();
3786                                 } else {
3787                                         submitWhenDoneProcessing = function () {
3788                                                 if ( 0 === processing() ) {
3789                                                         api.state.unbind( 'change', submitWhenDoneProcessing );
3790                                                         submit();
3791                                                 }
3792                                         };
3793                                         api.state.bind( 'change', submitWhenDoneProcessing );
3794                                 }
3795
3796                         }
3797                 });
3798
3799                 // Refresh the nonces if the preview sends updated nonces over.
3800                 api.previewer.bind( 'nonce', function( nonce ) {
3801                         $.extend( this.nonce, nonce );
3802                 });
3803
3804                 // Refresh the nonces if login sends updated nonces over.
3805                 api.bind( 'nonce-refresh', function( nonce ) {
3806                         $.extend( api.settings.nonce, nonce );
3807                         $.extend( api.previewer.nonce, nonce );
3808                         api.previewer.send( 'nonce-refresh', nonce );
3809                 });
3810
3811                 // Create Settings
3812                 $.each( api.settings.settings, function( id, data ) {
3813                         var constructor = api.settingConstructor[ data.type ] || api.Setting,
3814                                 setting;
3815
3816                         setting = new constructor( id, data.value, {
3817                                 transport: data.transport,
3818                                 previewer: api.previewer,
3819                                 dirty: !! data.dirty
3820                         } );
3821                         api.add( id, setting );
3822                 });
3823
3824                 // Create Panels
3825                 $.each( api.settings.panels, function ( id, data ) {
3826                         var constructor = api.panelConstructor[ data.type ] || api.Panel,
3827                                 panel;
3828
3829                         panel = new constructor( id, {
3830                                 params: data
3831                         } );
3832                         api.panel.add( id, panel );
3833                 });
3834
3835                 // Create Sections
3836                 $.each( api.settings.sections, function ( id, data ) {
3837                         var constructor = api.sectionConstructor[ data.type ] || api.Section,
3838                                 section;
3839
3840                         section = new constructor( id, {
3841                                 params: data
3842                         } );
3843                         api.section.add( id, section );
3844                 });
3845
3846                 // Create Controls
3847                 $.each( api.settings.controls, function( id, data ) {
3848                         var constructor = api.controlConstructor[ data.type ] || api.Control,
3849                                 control;
3850
3851                         control = new constructor( id, {
3852                                 params: data,
3853                                 previewer: api.previewer
3854                         } );
3855                         api.control.add( id, control );
3856                 });
3857
3858                 // Focus the autofocused element
3859                 _.each( [ 'panel', 'section', 'control' ], function( type ) {
3860                         var id = api.settings.autofocus[ type ];
3861                         if ( ! id ) {
3862                                 return;
3863                         }
3864
3865                         /*
3866                          * Defer focus until:
3867                          * 1. The panel, section, or control exists (especially for dynamically-created ones).
3868                          * 2. The instance is embedded in the document (and so is focusable).
3869                          * 3. The preview has finished loading so that the active states have been set.
3870                          */
3871                         api[ type ]( id, function( instance ) {
3872                                 instance.deferred.embedded.done( function() {
3873                                         api.previewer.deferred.active.done( function() {
3874                                                 instance.focus();
3875                                         });
3876                                 });
3877                         });
3878                 });
3879
3880                 api.bind( 'ready', api.reflowPaneContents );
3881                 $( [ api.panel, api.section, api.control ] ).each( function ( i, values ) {
3882                         var debouncedReflowPaneContents = _.debounce( api.reflowPaneContents, 100 );
3883                         values.bind( 'add', debouncedReflowPaneContents );
3884                         values.bind( 'change', debouncedReflowPaneContents );
3885                         values.bind( 'remove', debouncedReflowPaneContents );
3886                 } );
3887
3888                 // Check if preview url is valid and load the preview frame.
3889                 if ( api.previewer.previewUrl() ) {
3890                         api.previewer.refresh();
3891                 } else {
3892                         api.previewer.previewUrl( api.settings.url.home );
3893                 }
3894
3895                 // Save and activated states
3896                 (function() {
3897                         var state = new api.Values(),
3898                                 saved = state.create( 'saved' ),
3899                                 activated = state.create( 'activated' ),
3900                                 processing = state.create( 'processing' );
3901
3902                         state.bind( 'change', function() {
3903                                 if ( ! activated() ) {
3904                                         saveBtn.val( api.l10n.activate ).prop( 'disabled', false );
3905                                         closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
3906
3907                                 } else if ( saved() ) {
3908                                         saveBtn.val( api.l10n.saved ).prop( 'disabled', true );
3909                                         closeBtn.find( '.screen-reader-text' ).text( api.l10n.close );
3910
3911                                 } else {
3912                                         saveBtn.val( api.l10n.save ).prop( 'disabled', false );
3913                                         closeBtn.find( '.screen-reader-text' ).text( api.l10n.cancel );
3914                                 }
3915                         });
3916
3917                         // Set default states.
3918                         saved( true );
3919                         activated( api.settings.theme.active );
3920                         processing( 0 );
3921
3922                         api.bind( 'change', function() {
3923                                 state('saved').set( false );
3924                         });
3925
3926                         api.bind( 'saved', function() {
3927                                 state('saved').set( true );
3928                                 state('activated').set( true );
3929                         });
3930
3931                         activated.bind( function( to ) {
3932                                 if ( to ) {
3933                                         api.trigger( 'activated' );
3934                                 }
3935                         });
3936
3937                         // Expose states to the API.
3938                         api.state = state;
3939                 }());
3940
3941                 // Button bindings.
3942                 saveBtn.click( function( event ) {
3943                         api.previewer.save();
3944                         event.preventDefault();
3945                 }).keydown( function( event ) {
3946                         if ( 9 === event.which ) // tab
3947                                 return;
3948                         if ( 13 === event.which ) // enter
3949                                 api.previewer.save();
3950                         event.preventDefault();
3951                 });
3952
3953                 closeBtn.keydown( function( event ) {
3954                         if ( 9 === event.which ) // tab
3955                                 return;
3956                         if ( 13 === event.which ) // enter
3957                                 this.click();
3958                         event.preventDefault();
3959                 });
3960
3961                 $( '.collapse-sidebar' ).on( 'click', function() {
3962                         if ( 'true' === $( this ).attr( 'aria-expanded' ) ) {
3963                                 $( this ).attr({ 'aria-expanded': 'false', 'aria-label': api.l10n.expandSidebar });
3964                         } else {
3965                                 $( this ).attr({ 'aria-expanded': 'true', 'aria-label': api.l10n.collapseSidebar });
3966                         }
3967
3968                         overlay.toggleClass( 'collapsed' ).toggleClass( 'expanded' );
3969                 });
3970
3971                 // Keyboard shortcuts - esc to exit section/panel.
3972                 $( 'body' ).on( 'keydown', function( event ) {
3973                         var collapsedObject, expandedControls = [], expandedSections = [], expandedPanels = [];
3974
3975                         if ( 27 !== event.which ) { // Esc.
3976                                 return;
3977                         }
3978
3979                         // Check for expanded expandable controls (e.g. widgets and nav menus items), sections, and panels.
3980                         api.control.each( function( control ) {
3981                                 if ( control.expanded && control.expanded() && _.isFunction( control.collapse ) ) {
3982                                         expandedControls.push( control );
3983                                 }
3984                         });
3985                         api.section.each( function( section ) {
3986                                 if ( section.expanded() ) {
3987                                         expandedSections.push( section );
3988                                 }
3989                         });
3990                         api.panel.each( function( panel ) {
3991                                 if ( panel.expanded() ) {
3992                                         expandedPanels.push( panel );
3993                                 }
3994                         });
3995
3996                         // Skip collapsing expanded controls if there are no expanded sections.
3997                         if ( expandedControls.length > 0 && 0 === expandedSections.length ) {
3998                                 expandedControls.length = 0;
3999                         }
4000
4001                         // Collapse the most granular expanded object.
4002                         collapsedObject = expandedControls[0] || expandedSections[0] || expandedPanels[0];
4003                         if ( collapsedObject ) {
4004                                 collapsedObject.collapse();
4005                                 event.preventDefault();
4006                         }
4007                 });
4008
4009                 $( '.customize-controls-preview-toggle' ).on( 'click', function() {
4010                         overlay.toggleClass( 'preview-only' );
4011                 });
4012
4013                 // Previewed device bindings.
4014                 api.previewedDevice = new api.Value();
4015
4016                 // Set the default device.
4017                 api.bind( 'ready', function() {
4018                         _.find( api.settings.previewableDevices, function( value, key ) {
4019                                 if ( true === value['default'] ) {
4020                                         api.previewedDevice.set( key );
4021                                         return true;
4022                                 }
4023                         } );
4024                 } );
4025
4026                 // Set the toggled device.
4027                 footerActions.find( '.devices button' ).on( 'click', function( event ) {
4028                         api.previewedDevice.set( $( event.currentTarget ).data( 'device' ) );
4029                 });
4030
4031                 // Bind device changes.
4032                 api.previewedDevice.bind( function( newDevice ) {
4033                         var overlay = $( '.wp-full-overlay' ),
4034                                 devices = '';
4035
4036                         footerActions.find( '.devices button' )
4037                                 .removeClass( 'active' )
4038                                 .attr( 'aria-pressed', false );
4039
4040                         footerActions.find( '.devices .preview-' + newDevice )
4041                                 .addClass( 'active' )
4042                                 .attr( 'aria-pressed', true );
4043
4044                         $.each( api.settings.previewableDevices, function( device ) {
4045                                 devices += ' preview-' + device;
4046                         } );
4047
4048                         overlay
4049                                 .removeClass( devices )
4050                                 .addClass( 'preview-' + newDevice );
4051                 } );
4052
4053                 // Bind site title display to the corresponding field.
4054                 if ( title.length ) {
4055                         api( 'blogname', function( setting ) {
4056                                 var updateTitle = function() {
4057                                         title.text( $.trim( setting() ) || api.l10n.untitledBlogName );
4058                                 };
4059                                 setting.bind( updateTitle );
4060                                 updateTitle();
4061                         } );
4062                 }
4063
4064                 /*
4065                  * Create a postMessage connection with a parent frame,
4066                  * in case the Customizer frame was opened with the Customize loader.
4067                  *
4068                  * @see wp.customize.Loader
4069                  */
4070                 parent = new api.Messenger({
4071                         url: api.settings.url.parent,
4072                         channel: 'loader'
4073                 });
4074
4075                 /*
4076                  * If we receive a 'back' event, we're inside an iframe.
4077                  * Send any clicks to the 'Return' link to the parent page.
4078                  */
4079                 parent.bind( 'back', function() {
4080                         closeBtn.on( 'click.customize-controls-close', function( event ) {
4081                                 event.preventDefault();
4082                                 parent.send( 'close' );
4083                         });
4084                 });
4085
4086                 // Prompt user with AYS dialog if leaving the Customizer with unsaved changes
4087                 $( window ).on( 'beforeunload', function () {
4088                         if ( ! api.state( 'saved' )() ) {
4089                                 setTimeout( function() {
4090                                         overlay.removeClass( 'customize-loading' );
4091                                 }, 1 );
4092                                 return api.l10n.saveAlert;
4093                         }
4094                 } );
4095
4096                 // Pass events through to the parent.
4097                 $.each( [ 'saved', 'change' ], function ( i, event ) {
4098                         api.bind( event, function() {
4099                                 parent.send( event );
4100                         });
4101                 } );
4102
4103                 // Pass titles to the parent
4104                 api.bind( 'title', function( newTitle ) {
4105                         parent.send( 'title', newTitle );
4106                 });
4107
4108                 // Initialize the connection with the parent frame.
4109                 parent.send( 'ready' );
4110
4111                 // Control visibility for default controls
4112                 $.each({
4113                         'background_image': {
4114                                 controls: [ 'background_repeat', 'background_position_x', 'background_attachment' ],
4115                                 callback: function( to ) { return !! to; }
4116                         },
4117                         'show_on_front': {
4118                                 controls: [ 'page_on_front', 'page_for_posts' ],
4119                                 callback: function( to ) { return 'page' === to; }
4120                         },
4121                         'header_textcolor': {
4122                                 controls: [ 'header_textcolor' ],
4123                                 callback: function( to ) { return 'blank' !== to; }
4124                         }
4125                 }, function( settingId, o ) {
4126                         api( settingId, function( setting ) {
4127                                 $.each( o.controls, function( i, controlId ) {
4128                                         api.control( controlId, function( control ) {
4129                                                 var visibility = function( to ) {
4130                                                         control.container.toggle( o.callback( to ) );
4131                                                 };
4132
4133                                                 visibility( setting.get() );
4134                                                 setting.bind( visibility );
4135                                         });
4136                                 });
4137                         });
4138                 });
4139
4140                 // Juggle the two controls that use header_textcolor
4141                 api.control( 'display_header_text', function( control ) {
4142                         var last = '';
4143
4144                         control.elements[0].unsync( api( 'header_textcolor' ) );
4145
4146                         control.element = new api.Element( control.container.find('input') );
4147                         control.element.set( 'blank' !== control.setting() );
4148
4149                         control.element.bind( function( to ) {
4150                                 if ( ! to )
4151                                         last = api( 'header_textcolor' ).get();
4152
4153                                 control.setting.set( to ? last : 'blank' );
4154                         });
4155
4156                         control.setting.bind( function( to ) {
4157                                 control.element.set( 'blank' !== to );
4158                         });
4159                 });
4160
4161                 // Change previewed URL to the homepage when changing the page_on_front.
4162                 api( 'show_on_front', 'page_on_front', function( showOnFront, pageOnFront ) {
4163                         var updatePreviewUrl = function() {
4164                                 if ( showOnFront() === 'page' && parseInt( pageOnFront(), 10 ) > 0 ) {
4165                                         api.previewer.previewUrl.set( api.settings.url.home );
4166                                 }
4167                         };
4168                         showOnFront.bind( updatePreviewUrl );
4169                         pageOnFront.bind( updatePreviewUrl );
4170                 });
4171
4172                 // Change the previewed URL to the selected page when changing the page_for_posts.
4173                 api( 'page_for_posts', function( setting ) {
4174                         setting.bind(function( pageId ) {
4175                                 pageId = parseInt( pageId, 10 );
4176                                 if ( pageId > 0 ) {
4177                                         api.previewer.previewUrl.set( api.settings.url.home + '?page_id=' + pageId );
4178                                 }
4179                         });
4180                 });
4181
4182                 // Update the setting validities.
4183                 api.previewer.bind( 'selective-refresh-setting-validities', function handleSelectiveRefreshedSettingValidities( settingValidities ) {
4184                         api._handleSettingValidities( {
4185                                 settingValidities: settingValidities,
4186                                 focusInvalidControl: false
4187                         } );
4188                 } );
4189
4190                 // Focus on the control that is associated with the given setting.
4191                 api.previewer.bind( 'focus-control-for-setting', function( settingId ) {
4192                         var matchedControl;
4193                         api.control.each( function( control ) {
4194                                 var settingIds = _.pluck( control.settings, 'id' );
4195                                 if ( -1 !== _.indexOf( settingIds, settingId ) ) {
4196                                         matchedControl = control;
4197                                 }
4198                         } );
4199
4200                         if ( matchedControl ) {
4201                                 matchedControl.focus();
4202                         }
4203                 } );
4204
4205                 // Refresh the preview when it requests.
4206                 api.previewer.bind( 'refresh', function() {
4207                         api.previewer.refresh();
4208                 });
4209
4210                 api.trigger( 'ready' );
4211
4212                 // Make sure left column gets focus
4213                 topFocus = closeBtn;
4214                 topFocus.focus();
4215                 setTimeout(function () {
4216                         topFocus.focus();
4217                 }, 200);
4218
4219         });
4220
4221 })( wp, jQuery );